我们之前讨论过KVO
的原理,知道KVO
机制是生成了一个中间类NSKVONotifying
,该中间类是被观察对象的原类的子类,被观察对象的isa
指针从指向原来的类改成指向新生成的中间类。而新生成的中间类,通过重写setter
方法将重写的信息回调给观察者。
那么根据这些信息,我们是否能够自定义一个KVO
呢?答案是可以的。下面我们就简单的模拟一个KVO
的实现。
确定方法在NSObject
的分类中实现
通过官方实现的KVO
可以看出,KVO
是用NSObject
的分类实现,其注册和移除方法是NSObject
的NSKeyValueObserving
分类,其回调方法是NSObject
的NSKeyValueObserverRegistration
分类。所以,我们要实现自定义,必须也是写一个NSObject
的分类方法。
@interface NSObject (TKVO)
@end
确定回调的信息
官方的KVO
分为三部曲: 添加观察者、回调更改、移除观察者。方法如下:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
其添加观察者、更改回调的方法是分离的,类似于代理,观察者实现了添加观察的方法,观察对象的更改通过另一个方法再传过来。这种传值方式我们可以使用delegate
、block
、notification
等实现。为了让方法的使用更加的简洁,此处我们用block
的回调实现。
更改回调方法里主要的参数如下:
- 观察对象
- 观察路径,也就是属性
-
change
也就是改变,包括旧值、新值等 -
context
上下文指针
我们可以定义一个block
,将这些数据都放进block
里进行回传。此处为了简单,就忽略掉context
。
typedef void(^TObserveingBlock) (id observerObject, NSString *observeKeyPath, id oldValue, id newValue);
确定注册方法名称、参数
然后我们再来看看注册观察者的方法,其参数如下:
- 观察者
- 观察路径,属性
- 设置回调的内容和时机
- 上下文指针
此处,我们只考虑最简单的情况,在回调的block
返回新值、旧值,所以我们自定义的方法就不考虑NSKeyValueObservingOptions
和context
了。
至此,我们自定义的方法参数就确定了:
- 观察则
- 观察路径
- 回调
block
方法如下:
- (void)addTObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(TObserveingBlock)block;
注册、回调方法的实现
根据KVO
的实现原理,我们要做以下处理:
- 因为
KVO
是基于KVC
的,所以检查被观察对象的类是否有相应的setter
方法,如果没有就抛出异常
- 因为
- 检查被观察对象
isa
指向的类是不是一个KVO
类,如果不是,新建一个继承于被观察对象的类的子类,并把isa
指向这个新建的子类
- 检查被观察对象
- 检查这个中间类是否重写过要观察属性的
setter
方法,如果没有,添加重写的setter
方法
- 检查这个中间类是否重写过要观察属性的
- 添加观察者
检查被观察的对象的类有没有相应的setter
方法,如果没有抛出异常
1.1、先通过下面方法获得相应的setter
的名字SEL
// 根据keyPath获取到setter name => setName
// 把key的首字母大写,前面加上set,key就变成了setKey,然后再加:
static NSString *setterFromKeyPath(NSString *kPath) {
if (kPath.length <= 0) {
return nil;
}
NSString *firstLetter = [[kPath substringToIndex:1] uppercaseString];
NSString *remianLetters = [kPath substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:", firstLetter, remianLetters];
}
1.2 然后使用class_getInstanceMethod
去获取setKey:
方法的实现,如果没有实现,需要抛出异常:
SEL setterSelector = NSSelectorFromString(setterFromKeyPath(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
if (!setterMethod) {
NSString *reason = [NSString stringWithFormat:
@"Object %@ does not have a setter for key %@", self, keyPath];
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
return;
}
创建中间类,修改isa
指向
2.1 获取到当前类名
Class currentClass = object_getClass(self);
NSString *currentClassName = NSStringFromClass(currentClass);
2.2 判断获取到的类名有没有我们定义的前缀。如果没有,我们就去创建新的中间类,继承于原来的类;并且修改被观察对象isa
的指向,让其指向新创建的中间类。
该静态常量是加给新创建类的类名前缀,用以标志中间类。
static NSString * const kTKVOClassPrefix = @"TKVONotifying_";
if (![currentClassName hasPrefix:kTKVOClassPrefix]) {
// 生成中间类
currentClass = [self makeKVOClassWithOriginalClassName:currentClassName];
// 修改原对象isa的指向
object_setClass(self, currentClass);
}
// 如果没有生成中间类,那就创建中间类
- (Class)makeKVOClassWithOriginalClassName:(NSString *)originalClassName {
// 生成kTKVOClassPrefix_class的类名
NSString *tKvoClassName = [kTKVOClassPrefix stringByAppendingString:originalClassName];
Class newClass = NSClassFromString(tKvoClassName);
// 如果kvo class已经被注册过了 直接返回
if (newClass) {
return newClass;
}
Class originClass = object_getClass(self);
Class tKvoClass = objc_allocateClassPair(originClass, tKvoClassName.UTF8String, 0);
// 重写中间类的class方法,学习Apple的做法,对外隐藏中间类kvo_class
Method tClassMethod = class_getInstanceMethod(originClass, @selector(class));
const char *types = method_getTypeEncoding(tClassMethod);
class_addMethod(tKvoClass, @selector(class), (IMP)kvo_class, types);
objc_registerClassPair(tKvoClass);
return tKvoClass;
}
static Class kvo_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
执行到这里, 被观察对象的isa
指针指向的已不是原类了, 而是新创建的中间类。
重写setter方法
中间类重写了好几个方法,其中最主要的就是setter
。现在就需要判断中间类是否已经重写了setter
,如果没有的话,就需要重写。
// 是否已经存在了`setter`方法, 不存在就添加一个
if (![self hasSelector:setterSelector]) {
const char *type = method_getTypeEncoding(setterMethod);
class_addMethod(currentClass, setterSelector, (IMP)kvo_setter, type);
}
// 判断当前类是否存在某个方法
- (BOOL)hasSelector:(SEL)selector {
Class currentClass = object_getClass(self);
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(currentClass, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
SEL thisSelector = method_getName(methodList[i]);
if (thisSelector == selector) {
free(methodList);
return YES;
}
}
free(methodList);
return NO;
}
重写的setter
方法就是属性更改之后回调的关键。新方法在调用原方法给父类的属性赋值之后,通知每个观察者(调用传入的block
)。
static void kvo_setter(id self, SEL _cmd, id newValue) {
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = getterForSetter(setterName);
// 如果不存在getter方法,就要抛出异常
if (!getterName) {
NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil];
return;
}
id oldValue = [self valueForKey:getterName];
// 调用原有类的setter方法
// 实现`objc_super`的结构体
struct objc_super tSuperclass = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
// 这里需要做个类型强转, 否则会报too many argument的错误
void (*objc_msgSendSuperCasted) (void *, SEL, id) = (void *)objc_msgSendSuper;
objc_msgSendSuperCasted(&tSuperclass, _cmd, newValue);
// 找出观察者的数组,调用对应对象的callback,给观察者回调
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kTKVOObservers));
for (TObserverInfo *info in observers) {
if ([info.keyPath isEqualToString:getterName]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
info.block(self, getterName, oldValue, newValue);
});
}
}
}
// 通过setter得到getter
static NSString *getterForSetter(NSString *setter) {
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
return nil;
}
NSRange range = NSMakeRange(3, setter.length - 4);
NSString *getter = [setter substringWithRange:range];
// lower case the first letter
NSString *firstLetter = [[getter substringToIndex:1] lowercaseString];
getter = [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1)
withString:firstLetter];
return getter;
}
定义一个类,来存储观察者的相关信息
@interface TObserverInfo: NSObject
// 此时使用weak防止循环引用
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) TObserveingBlock block;
@end
@implementation TObserverInfo
- (instancetype)initWithObserver:(NSObject *)ob keyPath:(NSString *)kp block:(TObserveingBlock)tob {
self = [super init];
if (self) {
_observer = ob;
_keyPath = kp;
_block = tob;
}
return self;
}
@end
存储观察者信息,用以回调、移除
将观察的相关信息(观察者,被观察的key
, 和回调的block
)封装在 TObserverInfo
类里,通过associatedObject
存储起来。
定义一个静态常量用来作为关联对象的key
:
static NSString * const kTKVOObservers = @"TKVOObservers";
存储信息:
TObserverInfo *info = [[TObserverInfo alloc] initWithObserver:observer keyPath:keyPath block:block];
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kTKVOObservers));
if (!observers) {
observers = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge const void *)(kTKVOObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[observers addObject:info];
到这里,我们自定义的KVO
,注册以及回调部分已经完成。下面我们来看看移除观察者。
移除观察者
移除观察者只是需要我们将对应的观察者从动态关联的观察者列表中删除即可,所以我们只需要传入观察者和观察路径便于区分即可:
- (void)removeTObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kTKVOObservers));
if ([observers count] == 0) {
return;
}
TObserverInfo *removeInfo;
for (TObserverInfo *info in observers) {
if (info.observer == observer && [info.keyPath isEqual:keyPath]) {
removeInfo = info;
break;
}
}
[observers removeObject:removeInfo];
}
移除观察者优化
KVO
一般在观察者调用dealloc
的时候进行移除观察者的操作,其实当我们观察的对象被销毁的时候,观察就没有意义了。所以我们也可以在观察对象dealloc
的时候移除观察操作。
仿照重写setter
的时候,我们重写一下父类的dealloc
方法。
SEL deallocSEL = NSSelectorFromString(@"dealloc");
if (![self hasSelector:deallocSEL]) {
Method deallocMethod = class_getInstanceMethod(class_getSuperclass(object_getClass(self)), deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(currentClass, deallocSEL, (IMP)kvo_dealloc, deallocTypes);
}
static void kvo_dealloc(id self, SEL _cmd) {
NSLog(@"===kvo_dealloc===");
Class superClass = class_getSuperclass(object_getClass(self));
object_setClass(self, superClass);
}
使用Method Swizzling
交换方法
在NSObjec+KVO
中重写load
方法,在这个方法中,使用一个自定义的释放方法交换父类的dealloc
方法。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 将父类的方法和自己重写的方法交换
[self exchangeOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(kvoDealloc)];
});
}
- (void)kvoDealloc {
Class superClass = class_getSuperclass(object_getClass(self));
object_setClass(self, superClass);
[self kvoDealloc];
}
总结
我们可以根据KVO
的原理进行简单的自定义KVO
,其思路如下:
- 确实自定义
KVO
是NSObject
的分类
- 确实自定义
- 确定注册方法的参数、回调更改的方式为
block
,回调的参数
- 确定注册方法的参数、回调更改的方式为
-
- 实现注册、回调方法
- 检查被观察对象的类是否有相应的
setter
方法,如果没有就抛出异常 - 检查被观察对象
isa
指向的类是不是一个KVO
类,如果不是,新建一个继承于被观察对象的类的子类,并把isa
指向这个新建的子类 - 检查这个中间类是否重写过要观察属性的
setter
方法,如果没有,重写的setter
方法,在setter
方法中实现更改的回调 - 添加观察者,使用关联对象存储观察的相关信息
- 实现移除观察者的方法,从关联对象的存储信息中心移除观察者
网友评论