iOS中KVO的底层实现原理
在开发中我们经常使用addObserver:forKeyPath:options:context:
方法来观察类的某个属性的改变,然后在observeValueForKeyPath:ofObject:change:context:
方法中监听改变的回调,其底层的实现原理大致如下:
- 利用
Runtime
动态的生成一个子类,类名是NSKVONotifying_
为前缀。 - 苹果为了隐藏KVO的实现,重写了子类
class
方法,返回的是父类的类对。 - 重写了被监听属性的
setter
方法,这是实现KVO
的关键,其实内部调用了Foundation
框架下的_NSSet***ValueAndNotify
方法,看具体监听属性的类型是什么,方法的调用名略有区别,这个方法的实现是KVO
的核心,其大致实现逻辑如下:- 调用
- (void)willChangeValueForKey:(NSString *)key
方法表明属性即将发生改变。 - 调用父类原来的
setter
方法的实现。 - 调用
- (void)didChangeValueForKey:(NSString *)key
方法表明属性已改变,其中这个方法里面会调用observeValueForKeyPath: ofObject: change: context:
方法告知父类监听的属性发生了改变。
- 调用
上面只是大致了说了下底层的实现流程,其实当然还有一些其他的善后工作要做,这里我们不在深入研究,有兴趣的可以利用查看源码并用
Runtime
打印监听前后类的方法列表以及实现进行跟踪验证。
自定义KVO的实现
上面已经简要介绍了KVO
的实现原理,现在我们仿照上面的原理自己写一个KVO
的实现,也大致分为以下几个步骤:
- 动态生成一个子类。
- 重写
setter
方法,在方法中,调用super
的setter
实现,并通知观察者。
首先定义一个NSObject(KVO)
的分类,然后仿照苹果一样定义一个- (void)wy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
方法来监听属性,方法的具体实现如下:
- (void)wy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
//1.利用 runtime,动态生成一个类
//1.1 创建self的子类
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [@"wykvo_" stringByAppendingString:oldClassName];
const char *newName = [newClassName UTF8String];
//创建一个类的class
Class MyClass = objc_allocateClassPair([self class], newName, 0);
//注册类
objc_registerClassPair(MyClass);
//2.添加一个set方法
class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@");
//3.改变isa指针(这个好像不利于把方法写成活的,采用方法交换可能更好)
object_setClass(self, MyClass);
//4.保存观察者对象
objc_setAssociatedObject(self, @"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
首先利用Runtime
的objc_allocateClassPair
方法来动态生成一个子类,并添加一个setName
的方法,并改变isa
指针的指向为新生成的子类,同时利用关联对象为分类添加一个objc
的属性保存着观察者对象以便后面通知观察者属性发生了改变。
这里有几个点需要注意,由于这里只是简单的模拟
name
属性的改变,所以set属性的方法名是写死的,实际上应该根据keypath
来动态确定,这里不在深入;为了实现当调用set
方法能调用到新类的set
方法上,采用了改变isa
的指针来实现,这样在调用时会根据isa
的指向找到新类的实现;同时由于分类中需要保存观察者,由于分类是不能添加属性的,这里采用了关联对象来保存观察者对象。
void setName(id self,SEL _cmd,NSString *newName){
//调用父类的set方法,需要在build打开容许消息机制
//保存子类类型
id class = [self class];
//改变self的isa指针
object_setClass(self, class_getSuperclass(class));
((void (*)(id, SEL, NSString *))objc_msgSend)(self, @selector(setName:), newName);
//拿到通知观察者
id objc = objc_getAssociatedObject(self, @"objc");
// 通知观察者
((void (*)(id, SEL, id, NSString *, id, void *))objc_msgSend)(objc, @selector(observeValueForKeyPath:ofObject:change:context:),self,@"name",nil,nil);
//改回子类类型
object_setClass(self, class);
}
在setName
的实现中,由于需要首先调用原来的set
实现,所以再次将isa
指针指向原来的被观察对象,同时利用objc_msgSend
消息发送机制调用set
方法,这样会根据isa
指针首先找到观察类的set
实现,然后通过关联对象拿到观察者,利用objc_msgSend
调用相应的方法完成通知。
注意点
在使用KVO
的过程中,判断某个属性设置会不会触发KVO
需要看是否调用了set
方法,比如如果直接对成员变量进行赋值则不会触发KVO
机制,比如Person
类里面一个dog
对象属性,dog
类有个name
属性吗,当我们监听dog
属性时,如何用person.dog.name
对dog
的name
进行赋值时则不会调用dog
的set
方法,是不会触发KVO
的,但是可以手动在person.dog.name
的前后调用上面提到的willChangeValueForKey
和didChangeValueForKey
方法来触发KVO
机制。
总结
根据KVO
的底层的实现原理,利用Runtime
的消息机制,isa
指针和关联对象等相关底层知识模仿实现了KVO
实现,这有助于进一步理解底层KVO
的实现原理,并加深对Runtime
的相关知识的理解。