0%

KVO

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以上不会崩溃。

手动处理变更通知

  1. 在被观察对象(如上面的BankObject)重写+automaticallyNotifiesObserversForKey:方法(默认返回YES,即默认所有键值都会通知变更),定义需要手动控制的属性。记得在不需要修改的key上返回super方法。
  2. 在需要发送通知的地方调用:
    • 一对一: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
2
3
4
5
6
// 注册与取消
func observe<Value>(_ keyPath: KeyPath<_KeyValueCodingAndObserving, Value>, options: NSKeyValueObservingOptions = [], changeHandler: @escaping (_KeyValueCodingAndObserving, NSKeyValueObservedChange<Value>) -> Void) -> NSKeyValueObservation
func invalidate()

// 使用
weather.observe(\.text, options: [.initial, .new, .old, .prior], changeHandler: printSwiftKVOReponse)

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通知。

参考

欢迎关注我的其它发布渠道