1. KVO基础知识
KVO全称为Key-Value Observing,也称为“键值监听”,主要用于监听某个对象的属性值的改变。
1.1 KVO的基本使用
1 | #import "ViewController.h" |
2. KVO的本质探究
2.1 KVO的实现原理
我们分别打印对象_person1和_person2的isa指针。结果如下:
1 | (lldb) po _person1->isa |
由于实例对象的isa指针指向类对象,所以_person1的类对象是NSKVONotifying_HLPerson,_person2的类对象是HLPerson。由此可见,如果给某个实例对象添加了KVO监听,那么它的isa指针会指向一个全新的类对象(NSKVONotifying_HLPerson),而如果不添加KVO监听的话,isa指针还是指向原来的类对象。
(1)未使用KVO监听的对象的底层实现如下图所示:
(2)使用了KVO监听的对象的底层实现如下图所示:
由上可知,KVO的实现原理是在运行时通过isa-swizzling技术动态地创建一个继承自当前类的派生类(NSKVONotifying_XXX),并且动态地修改当前实例对象的isa指针,使其指向派生类(NSKVONotifying_XXX),并重写了class方法,使其返回当前类的Class。派生类NSKVONotifying_HLPerson还重写了父类当中的setAge:方法,调用了Foundation中的_NSSetIntValueAndNotify(_NSSet*ValueAndNotify,可能是Int也可能是double),那么_NSSetIntValueAndNotify函数内部是如何实现的呢? 先调用willChangeValueForKey:方法,再调用父类当中的setAge:方法修改属性age的值,最后调用didChangeValueForKey:方法,didChangeValueForKey内部会触发监听器的监听回调函数(observeValueForKeyPath:key ofObjet:self change:nil context:nil)。
NSKVONotifying_HLPerson中的伪代码如下:
1 | //重写了class方法、dealloc方法、_isKVOA方法、setAge方法 |
2.2 派生类中重写了哪些方法?
派生类(NSKVONotifying_HLPerson)中重写了setAge方法、class方法、dealloc方法、_isKVOA方法。那么为什么苹果要重写class方法呢?因为苹果可能不想将生成的派生类(NSKVONotifying_HLPerson)暴露给开发者,所以采用重写-(Class)class方法的方式,屏蔽了内部实现,隐藏了动态生成的派生类(NSKVONotifying_HLPerson)。
如何验证派生类中重写了这些方法呢?可以通过以下代码来验证:
1 | //cls参数表示类名 |
调用printMethodNamesOfClass:方法
1 | [self printMethodNamesOfClass:object_getClass(_person1)]; |
打印结果如下:
1 | cls = NSKVONotifying_HLPerson,methodNames = setAge:,class,dealloc,_isKVOA, |
【总结】由打印结果可知,未添加KVO监听的实例对象的类对象中实现了属性的setter和getter方法,而添加了KVO监听所生成的派生类NSKVONotifying_HLPerson中实现了setAge:方法、class方法、dealloc方法、_isKVOA方法。
3. KVO总结
题目1:iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
利用Runtime动态生成一个子类,并且让instance对象的isa指向这个全新的子类,并且这个全新的子类重写了setter方法、class方法、dealloc方法、_isKVOA方法。当修改instance对象的属性时,重写的setter方法会调用Foundation的_NSSetXXXValueAndNotify函数,在_NSSetXXXValueAndNotify函数内部会先调用willChangeValueForKey:方法,然后调用父类原来的setter方法,最后调用didChangeValueForKey:方法,并在didChangeValueForKey方法内部触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)。
题目2:如何手动触发KVO?
手动调用willChangeValueForKey:和didChangeValueForKey:这两个方法即可。
题目3:直接修改成员变量会触发KVO吗?
不会触发KVO(直接修改成员变量不会调用该属性的setter方法)。KVO的实现原理是利用Runtime动态生成一个子类,并且让instance对象的isa指向这个全新的子类,并且这个全新的子类重写了setter方法、class方法、dealloc方法、_isKVOA方法。当修改instance对象的属性时,重写的setter方法会调用Foundation的_NSSetXXXValueAndNotify函数,在_NSSetXXXValueAndNotify函数内部会先调用willChangeValueForKey:方法,然后调用父类原来的setter方法,最后调用didChangeValueForKey:方法,并在didChangeValueForKey方法内部触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)。由于直接修改成员变量不会调用该属性的setter方法,所以不会触发KVO。
此时我们可以手动触发KVO监听。也就是手动调用willChangeValueForKey:和didChangeValueForKey:这两个方法。