KVO用于逻辑隔离对象之间的监听,支持一对一和一对多的属性监听。这里的一对一和一对多是针对监听的属性的,即既可以监听单个属性,也可以监听集合属性。
在OC中,所有NSObject子类的所有属性(包括计算属性)都支持KVO;而在Swift中,只有在@objc dynamic
修饰的属性(包括计算属性)才支持KVO,即使用@objc dynamic
修饰的属性与OC行为一致。
KVC和KVO都属于键值编程,而且底层实现机制都是isa-swizzing。
KVO和NSNotificationCenter都是iOS观察着模式的一种实现。KVO对被监听对象无侵入性,即无需修改代码即可支持监听。
使用
注册监听:
1 | [bankInstance addObserver:personInstance forKeyPath:@"accountBalance" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; |
bankInstance
是变更发出的对象。personInstance
是响应变更的对象。@"accountBalance"
是bankInstance
的key path。
选项是多选的,影响change字典内容和生成通知的方式。
- new:change字典包含新值
- old:change字典包含旧值
- initial:注册监听时即发送变更通知
- prior:在变更通知前发送一次变更前的通知,即每个变更两次通知,一个willChange,一个didChange
change字典的键:
- kind:改变类型
- setting:对象或集合赋值替换
- insertion:对象插入到集合属性中
- removal:对象从集合中移除
- replacement:对象从集合中替换
- new:新值
- old:旧值
- indexes:插入/移除/替换对象的索引
- prior:标识该通知是willChange的,否则是didChange的
new、old可以时单个对象,也可以时对象集合,表示集合时表示移除/替换的对象。
移除监听可在这两个时机完成:
- 不需要监听时。
- 被观察对象、观察者对象销毁时。被观察对象与观察者对象常常具有相同的生命周期。
使用注意:
移除监听的调用角色与注册监听保持一致。
多次注册监听导致多次响应。didChnage变更通知的顺序与注册顺序相反,即以先进后出的顺序调用。
注册监听与移除监听必须保持配对,多了少了都会导致异常崩溃。但iOS 11以上不会崩溃。
手动处理变更通知
- 在被观察对象(如上面的BankObject)重写
+automaticallyNotifiesObserversForKey:
方法(默认返回YES,即默认所有键值都会通知变更),定义需要手动控制的属性。记得在不需要修改的key上返回super方法。 - 在需要发送通知的地方调用:
- 一对一:
willChangeValueForKey:
、didChangeValueForKey:
- 有序一对多:
willChange:valuesAtIndexes:forKey:
、didChange:valuesAtIndexes:forKey:
- 无序一对多:
willChangeValueForKey:withSetMutation:usingObjects:
、didChangeValueForKey:withSetMutation:usingObjects:
- 一对一:
如果没有重写相关通知方法,则只会在initial选项生效。
手动处理变更通知的应用场景:
- 筛选通知,如去重、控制通知发送时机。
- 多个键的通知一起发送。
注册依赖键
需要在被观察对象(如上面的BankObject)重写+keyPathsForValuesAffectingValueForKey:
方法或+keyPathsForValuesAffecting<Key>
方法,返回给定key依赖的key path集合。记得在不需要修改的key上返回super方法。
注意:+keyPathsForValuesAffecting<Key>
方法在Swift中要声明为@objc
。否则不会识别。这种方法还可以用于在分类添加依赖键。
注册集合监听
对不可变集合采用一对一的对象赋值方式更新,对于可变集合,访问集合时,不能直接访问对应属性,需要使用mutableArrayValueForKey:
类似的方法取出集合,对取出的集合进行增删改都会出发变更通知。
集合注册依赖键比较麻烦,需要使用KVC的方式。
新特性与坑
Swift重新封装了注册和移除监听的方式:
1 | // 注册与取消 |
iOS 11以下使用时,需要主动置空NSKeyValueObservation或调用invalidate
方法。
Swift的KVO方式弱化了observer的存在,而是使用block接收变更通知。并返回一个token,用token进行取消注册。进而替代原有的注册与取消监听的方式。而其他方法observing对象的方法基本不变。
另外@objc dynamic
只支持与OC共用的类型,像Swift的枚举、结构体、元组都不支持。
监听OC的枚举类型还会出问题,导致change的所有值都为空,而使用传统的注册监听方式是正常的。
即使测试中监听可选类型的属性也会导致上述问题。
KVO实现原理
使用runtime创建被观察对象的子类,重写sttter,附加KVO通知逻辑,然后把isa
指针指向创建的子类并重写相关方法,实现对原本对象的替换。
在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类,并将class方法重写,返回原类的Class。当修改实例对象属性时,会调用Foundation的_NSSetXXXValueAndNotify
函数,函数先调用willChangeValueForKey:
,然后调用父类原来的setter方法修改值,最后是didChangeValueForKey:
,这些方法触发Observer的监听方法。
触发机制
所以,基于原理得知,所有调用属性setter的都会触发KVO通知,所以KVC也会触发KVO,但直接修改成员变量则不会。
注意:使用KVC能对没有setter的属性,甚至成员变量修改值,这都会触发相同keyPath的KVO通知。
参考
- 在Swift中使用KVO:Using Key-Value Observing in Swift
- 关于KVO看这篇就够了 | 殷永振