KVO的全称是Key-Value Observing,也称“键值监听”,可以用于监听某个对象属性值的改变。
使用KVO
通常我们通过addObserver:forKeyPath:options:context
来监听某个实例的某个属性变化。当该属性的值发生变化的时候会通过- (**void**)observeValueForKeyPath:(NSString *)keyPath ofObject:(**id**)object change:(NSDictionary<NSKeyValueChangeKey,**id**> *)change context:(**void** *)context
通知我们。
显然实现这个功能的基础是属性值得改变,而属性值得改变需要调用setter方法。可是即使setter方法是我们重写的,并且在其实现中并没有编写关于监听属性改变通知监听的代码,KVO依然可以起作用。这是为什么呢?
KVO实现原理
我们都知道一个类的实例方法存在于其类对象中,当一个类的实例向实例方法发送调用消息时候,会通过类实例的isa指针去其类对象中找到其对应的实例方法执行。setter方法就存在于类对象中。
新的类对象
我们通过runtime的object_getClass()
直接利用实例对象的isa指针查找实例的类对象。
这里之所以使用runtime获取类对象而不使用消息发送机制,是因为class是通过消息发送查找到class方法,在class方法中返回的类对象,而class方法可能被重写。
通过上图我们可以看出,使用KVO监听的属性的isa指向的类对象已经不是Person了,而是一个继承自Person的一个新的类NSKVONotifying_Person,我们并没有创建过这个类,它是objc的runtime动态生成的。
NSKVONotifying_Person重写了父类的setAge:
、class
、dealloc
方法,并且新增了一个_isKVOA
方法。
NSKVONotifying_Person的setter方法
我们可以看出在为person实例添加observer之前和之后的setAge:
方法的实现发生了变化,之前是直接调用类对象中存储的实例方法,添加之后调用了Foundation框架中_NSSetIntValueAndNotify
函数。
之所以是_NSSet*IntValue*AndNotify是因为age属性我们定义的是int类型,若是其他类型则会相应更改
在_NSSetIntValueAndNotify
方法中,会调用willChangeValueForKey
,然后调用父类的setAge
方法,再调用didChangeValueForKey
方法。在didChangeValueForKey
中通知属性改变,从而使得observeValueForKeyPath
得到消息。
NSKVONotifying_Person的class方法
在我们获取person实例的类对象的时候,我们发现使用runtime的object_getClass
和使用消息发送机制[_person class]
得到的结果不一样,这是因为NSKVONotifying_Person重写了class方法的实现,在实现中调用了[super class]
,而NSKVONotifying_Person又是Person的子类,所以获取到的Person。
NSKVONotifying_Person的dealloc方法
由于runtime在实例对象添加了KVO之后动态创建了类和一些对象,所以可能会在dealloc中回收这些资源。
NSKVONotifying_Person的_isKVOA方法
_isKVOA 应该是会返回一个BOOL类型的值,表示是否使用了KVO。
手动触发KVO
通过上面我们可以得知,KVO的实现主要是因为在setter方法中调用_NSSetIntValueAndNotify
,而它调用willChangeValueForKey
,然后调用父类的setAge
方法,再调用didChangeValueForKey
方法,didChangeValueForKey
告诉observeValueForKeyPath
属性改变。所以我们可以通过实现will和did方法手动出发KVO。
成员变量会不会触发KVO
通过上面原理我们可以知道,KVO主要是通过重写setter方法实现的,而成员变量并不会实现setter方法,所以成员变量的改变并不会触发KVO。