KVO的实现原理
首先需要了解KVO基本使用,KVO的全称 Key-Value Observing
,俗称“键值监听”,可以用于监听某个对象属性值的改变。
示例代码
1 | - (void)viewDidLoad { |
1 | // 打印内容为 |
上述代码中可以看出,在添加监听之后,age
属性的值在发生改变时,就会通知到监听者,执行监听者的 observeValueForKeyPath
方法。
KVO原理分析
通过上述代码我们可以发现,一旦 age
属性的值发生改变时,就会通知到监听者,而且我们知道赋值都是调用属性的 set
方法,我们可以来到 People
类中重写 age
的 set
方法,观察是否是KVO在 set
方法中做了什么操作来实现。
我们发现即使重写了 set
方法,p1对象和p2对象调用同样的 set
方法,但是我们发现p1除了调用set方法之外还会另外执行监听器的 observeValueForKeyPath
方法。
说明KVO在运行时获取对p1对象做了一些改变。相当于在程序运行过程中,对p1对象做了一些变化,使得p1对象在调用 setage
方法的时候可能做了一些额外的操作,所以问题出在对象身上,两个对象在内存中肯定不一样,两个对象可能本质上并不一样。接下来来探索KVO内部是怎么实现的。
KVO底层实现分析
我们在移除监听前打个断点,打印一下两个对象指向的类对象
1 | (lldb) po p1->isa |
我们可以发现,p1对象执行过 addObserver
操作之后,p1对象的 isa
指针由之前的指向类对象 People
变为指向 NSKVONotifying_People
类对象,而p2对象没有任何改变。也就是说一旦p1对象添加了KVO监听以后,其 isa
指针就会发生变化,因此 set
方法的执行效果就不一样了。
我们发现p1对象的 isa
指针在经过KVO监听之后已经指向了 NSKVONotifying_People
类对象,NSKVONotifying_People
其实是 People
的子类,那么也就是说其 superclass
指针是指向 People
类对象的,NSKVONotifying_People
是runtime在运行时生成的。那么p1对象在调用 setage
方法的时候,肯定会根据p1的 isa
找到 NSKVONotifying_People
,在 NSKVONotifying_People
中找 setage
的方法及实现。
经过查阅资料了解到 NSKVONotifying_People
中的 setage
方法中其实调用了Fundation
框架中C语言函数 _NSsetIntValueAndNotify
,_NSsetIntValueAndNotify
内部做的操作相当于,首先调用 willChangeValueForKey
将要改变方法,之后调用父类的 setage
方法对成员变量赋值,最后调用 didChangeValueForKey
已经改变方法。didChangeValueForKey
中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath
方法中。
KVO实现方式的验证
首先经过之前打断点打印 isa
指针,我们已经验证了,在执行添加监听的方法时,会将 isa
指针指向一个通过 runtime
创建的 People
的子类 NSKVONotifying_People
。另外我们可以通过打印方法实现的地址来看一下p1和p2的 setage
的方法实现的地址在添加KVO前后有什么变化。
1 | NSLog(@"添加KVO前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]); |
1 | // 输出 |
我们发现在添加KVO监听之前,p1和p2的 setAge
方法实现的地址相同,而经过KVO监听之后,p1的 setAge
方法实现的地址发生了变化,我们通过打印方法实现来看一下前后的变化发现,确实如我们上面所讲的一样,p1的 setAge
方法的实现由 People
类方法中的 setAge
方法转换为了C语言的 Foundation
框架的 _NSsetIntValueAndNotify
函数。
Foundation
框架中会根据属性的类型,调用不同的方法。例如我们之前定义的int类型的age属性,那么我们看到Foundation框架中调用的_NSsetIntValueAndNotify函数。那么我们把age的属性类型变为double重新打印一遍
1 | // 输出 |
我们发现调用的函数变为了 _NSSetDoubleValueAndNotify
,那么这说明 Foundation
框架中有许多此类型的函数,通过属性的不同类型调用不同的函数。
那么我们可以推测 Foundation
框架中还有很多例如 _NSSetBoolValueAndNotify
、_NSSetCharValueAndNotify
、 _NSSetFloatValueAndNotify
、 _NSSetLongValueAndNotify
等等函数。
NSKVONotifying_People内部结构
首先我们知道 NSKVONotifying_People
作为 People
的子类,其 superclass
指针指向 People
类,并且NSKVONotifyin_Person内部一定对setAge
方法做了单独的实现,那么 NSKVONotifying_People
同 People
类的差别可能就在于其内存储的对象方法及实现不同。
我们通过 runtime
分别打印 People
类对象和 NSKVONotifying_People
类对象内存储的对象方法
1 | - (void)printMethodNamesOfClass:(Class)cls { |
1 | // 输出 |
这里 NSKVONotifying_People
重写 class
方法是为了隐藏NSKVONotifying_People
。不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的 class
可以发现他们都返回 People
。
如果 NSKVONotifying_People
不重写 class
方法,那么当对象要调用 class
对象方法的时候就会一直向上找来到 nsobject
,而 nsobect
的 class
的实现大致为返回自己 isa
指向的类,返回p1的 isa
指向的类那么打印出来的类就是NSKVONotifying_People
,但是apple不希望将 NSKVONotifying_People
类暴露出来,并且不希望我们知道NSKVONotifying_People
内部实现,所以在内部重写了 class
方法,直接返回 People
类,所以外界在调用p1的 class
对象方法时,是 People
类。这样p1给外界的感觉p1还是 People
类,并不知道 NSKVONotifying_People
子类的存在。
验证didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法
我们重写 People
的 willChangeValueForKey:
和 didChangeValueForKey:
方法
1 | - (void)setAge:(int)age { |
1 | willChangeValueForKey: - begin |
再次运行来查看 didChangeValueForKey
的方法内运行过程,通过打印内容可以看到,确实在 didChangeValueForKey
方法内部已经调用了observer的 observeValueForKeyPath:ofObject:change:context:
方法。
手动触发
1 | [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; |
1 | // 输出 |
通过打印我们可以发现,didChangeValueForKey
方法内部成功调用了observeValueForKeyPath:ofObject:change:context:
,并且age的值并没有发生改变。
如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。