导语:
如果对KVO原理不是很熟悉的,可以参考下另一篇文章《ios KVO原理探究》,主要是通过模拟KVO底层实现原理来写一个简单的KVO,以便加深对KVO的理解,主要是通过Rumtime来实现。
Demo源码见KVOCustom
NS_ASSUME_NONNULL_BEGIN
@interface KVOModel : NSObject
@property (nonatomic, strong) NSString* name;
@end
NS_ASSUME_NONNULL_END
有个KVOModel,需要观察name属性的变化 ,自己写一个KVO,就不能用苹果提供的KVO,用KVO最重要是注册方法addObserver:forKeyPath:options:context:
,可以先查看下注册方法,看下方法的定义:
NSObject(NSKeyValueObserverRegistration)
,所以首先可以定义一个NSOject的分类,添加跟方法
addObserver:forKeyPath:options:context:
类似的方法,然后在这个方法中作如下修改:
1. 创建一个子类
2. 重写setName方法
3. 修改isa指针
4. 将观察者保存到当前对象
创建一个子类
用runtime的objc_allocateClassPair
方法给KVOModel创建一个子类,子类名为customKVO_KVOModel,并利用函数objc_registerClassPair注册这个子类,代码如下:
- (void)customKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
{
// 创建一个类
NSString* oldClassName = NSStringFromClass(self.class);
NSString* newClassName = [@"customKVO_" stringByAppendingString:oldClassName];
Class subClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
// 注册类
objc_registerClassPair(subClass);
}
重写setName方法
利用runtime的class_addMethod
方法重写setName方法,就是给子类customKVO_KVOModel添加函数setName。
// 重写setName方法
class_addMethod(subClass, @selector(setName:), (IMP)setName, "v@:@");
任何一个OC方法的调用,都会被系统转换为objc_msgSend函数处理,如下代码:
_model = [KVOModel alloc];
_model = [_model init];
其中在调用init方法的时候,系统会转换成如下代码:
_model = objc_msgSend(_model, @selector(init));
底层其实是一个init函数,有两个参数,一个是调用者,另一个就是方法编号,也就是说oc中任何一个没有参数的调用,都会有两个参数被传进去,oc函数默认有两个参数。要验证objc_msgSend需要改下编译环境,如下:
当然也可以在任何一个oc函数中直接用隐式参数self和_cmd,所以在重写方法的时候,重写方法setName的前两个参数为id self, SEL _cmd
,函数的声明如下:
void setName(id self, SEL _cmd, NSString* newName);
修改isa指针
用函数object_setClass可以做到更改KVOModel的isa指针,如下:
// 修改isa指针
object_setClass(self, subClass);
现在就可以通过调用自定义函数customKVO_addObserver:forKeyPath:options:context:
查看是否修改成功,
将观察者保存到当前对象
用函数objc_setAssociatedObject将观察者保存到当前对象,如下:
// 将观察者保存到当前对象
objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_ASSIGN);
这边只能用OBJC_ASSOCIATION_ASSIGN
,否则会造成循环引用,然后就可以在重写的setName方法中拿到observer对象,在重写setName方法中调用KVOModel类的setName方法,然后在通知observer,也就是调用observeValueForKeyPath:ofObject:change:context:
方法,我们也可以在setName判断observer是否为空,就可以避免因为在dealloc没有remove观察导致crash。重写的setName代码如下:
void setName(id self, SEL _cmd, NSString* newName)
{
// 调用父类的setName方法
Class class = [self class];
object_setClass(self, class_getSuperclass(class));
objc_msgSend(self, @selector(setName:), newName);
// 通知observer
id observer = objc_getAssociatedObject(self, @"observer");
if (observer)
{
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"customKVO_new:":newName, @"customKVO_kind":@1}, nil);
}
// self改回子类
object_setClass(self, class);
}
结尾
模拟系统的KVO,只是简单的逻辑过程,苹果的KVO实现上有很多很多的判断,有很多很多的功能,但是内部的原理大致是这样。