KVO
基础
官方文档:
Introduction to Key-Value Observing Programming Guide
开启 KVO 需要严格遵循以下 3 个步骤:
- 使用
addObserver:forKeyPath:options:context:
方法注册监听者; - 在监听类中实现
observeValueForKeyPath:ofObject:change:context:
方法来接收通知; - 当不需要接收时,需要调用
removeObserver:forKeyPath:
。在监听者dealloc
方法中需要调用这个方法来移除监听。
其它:
automaticallyNotifiesObserversForKey:
默认返回 YES
,当重写并对某个 Key
返回 NO
时,那么修改属性时就需要手动调用 (void)willChangeValueForKey:(NSString *)key
与 -(void)didChangeValueForKey:(NSString *)key
发送通知,我们也可以通过这样在 Setter
方法判断对象是否真的发生改变,只有真的发生改变时才发送通知。
KVO 详解
这篇文章非常详细,从 KVO
的使用到原理都进行了说明。
KVC 和 KVO
一个需要注意的地方是,KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 RunLoop 的处理。
objcio/issue-7-lab-color-space-explorer
Friday Q&A About KVO
mikeash.com: Friday Q&A 2009-01-23
Mikeash 关于 KVO 原理的文章:
- 动态生成一个
KVO
的子类,实现了dealloc
,_isKVOA
,class
方法; - 只会生成一个
KVO
子类,对所有监听的属性的设置方法都进行了替换,如果针对不同的属性监听生成不同类,就需要动态生成大量的不同的类,所以苹果选择了只生成一个类; - 替换了对应的方法的
IMP
,改用内部的NSSet...ValueAndNotify
;
Key-Value Observing Done Right
mikeash.com: Key-Value Observing Done Right
Mikeash 先是吹捧了一下 KVO 机制,非常强大和好用,但是 API 设计非常糟糕:
-addObserver:forKeyPath:options:context:
不支持selector
参数,对比NSNotificationCenter
的设计,可谓高下立判, KVO 必须要在-observeValueForKeyPath:ofObject:change:context:
中处理消息或者传递给父类;- 因为不支持
selector
参数,所以如果在相同的observer
监听相同的KeyPath
时,需要通过context
参数来进行区分; -removeObserver:forKeyPath:
不支持context
参数, KVO 是在 iOS2.0 时增加的,后面在 iOS5.0 新增了-removeObserver:forKeyPath:context:
,支持context
参数。
KVO Considered Harmful
KVO 缺点:
- 所有回调都在同一个方法中进行,稍不留意这个方法就会快速膨胀;
- 使用字符串硬编码,如果被监听的对象修改了属性名,编译期无法察觉;
- 要求处理父类的 KVO 流程;
- 移除 observer 时有可能会崩溃;
- 充斥着大量有可能会失败的操作,作者认为一个好的 API 设计应该起到使用者成功地调用他们,即使没有解释为什么要这样去调用;
- 流程过于隐藏,没办法追踪数据改变的流程,与 delegate 模式相比, KVO 在 debug 时比较麻烦,且需要在运行时通过
isKindOfClass:
动态判断类型; - 有可能造成死循环,如果不小心在回调中修改了监听的属性,那么就会造成死循环,如果说两个属性在不同的 KVO 流程中互相修改,也会造成死循环,且难于 debug ;
- KVO 在某些场景下会失效,比如说
__weak
属性,在__weak
对象被释放时, KVO 是不会去清理对应的监听,导致可能会出现野指针崩溃; - KVO 是一种老旧的模式,在 Apple 平台上,我们可以通过其它方式比如说 Delegate ,Block 和明确的发布/订阅 (
NSNotificationCenter
)方式来解决问题,而不是使用 KVO 这种隐晦的方式。
什么时候可以使用 KVO :
- Apple 官方要求,比如说
AVPlayer
,要求通过监听status
属性来获取播放器的状态; - 设计相关的 API 给其他开发者使用。
刨根问题 KVO 原理
通过源码相关的伪代码来探究 KVO
的实现方式,如果需要深入了解 KVO
的原理,可以阅读下这篇文章。 KVO
的原理看起来虽然比较简单,但是实现时还是有不少坑,比如说多线程,系统的具体实现也体现了这一点,通过 pthread_mutex_lock
来保证线程安全。
KVOController 解析
为了解决 KVO
非常难用的问题,Facebook 开源了 KVOController
,优点如下:
- 不需要手动移除
observer
,这里利用了关联属性在对象释放时也会被释放的原理,在关联属性的dealloc
方法中移除observer
; - 支持使用
block
,减少复杂度,添加监听和处理通知的代码可以放在同一处。
基于 KVO hook 子类的方法
在做 ViewController
的耗时检测时,我们需要记录各个 UIViewController
子类对应方法的耗时,如果只是针对 UIViewController
的方法进行 hook
,那么只能记录到 UIViewController
的方法耗时,无法获取子类的方法耗时。
在进行 KVO
时 runtime
实际上会帮你创建一个 KVO
相关的子类,由此可以在初始化时进行一次 KVO
来生成一个新的子类,然后对这个子类方法进行耗时检测。
至于为什么使用 KVO
的方式,下面这篇文章有进行解释,而且也给出了具体实现代码:
KVO 在不同的二进制中多个符号并存的 Crash 问题
当两个产物都有相同的类名时,比如主二进制和动态库中,这两个类都会被 realize ,都能够被正常调用。
其原因在于苹果使用的是
two level namespace
的技术。在这种形式下,符号所在的“库”的名称也会作为符号的一部分。链接的时候,staic linker
会标记住在这个符号是来自于哪个库的。这样不仅大大减少了dyld搜索符号所需要的时间,也更好对后续库的更新进行了兼容。
但是由于全局类表的存在,在动态创建 KVO
的子类时,只能产生一个。所以就导致 allocate
失败,从而引发 register
过程的 Crash 问题。