KVO

基础

官方文档:

Introduction to Key-Value Observing Programming Guide

开启 KVO 需要严格遵循以下 3 个步骤:

  1. 使用 addObserver:forKeyPath:options:context: 方法注册监听者;
  2. 在监听类中实现 observeValueForKeyPath:ofObject:change:context: 方法来接收通知;
  3. 当不需要接收时,需要调用 removeObserver:forKeyPath: 。在监听者 dealloc 方法中需要调用这个方法来移除监听。

其它:

automaticallyNotifiesObserversForKey: 默认返回 YES ,当重写并对某个 Key 返回 NO 时,那么修改属性时就需要手动调用 (void)willChangeValueForKey:(NSString *)key-(void)didChangeValueForKey:(NSString *)key 发送通知,我们也可以通过这样在 Setter 方法判断对象是否真的发生改变,只有真的发生改变时才发送通知。

KVO 详解

iOS 中的 KVO

这篇文章非常详细,从 KVO 的使用到原理都进行了说明。

KVC 和 KVO

ObjC 中国 - 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_isKVOAclass 方法;
  • 只会生成一个 KVO 子类,对所有监听的属性的设置方法都进行了替换,如果针对不同的属性监听生成不同类,就需要动态生成大量的不同的类,所以苹果选择了只生成一个类;
  • 替换了对应的方法的 IMP ,改用内部的 NSSet...ValueAndNotify

Key-Value Observing Done Right

mikeash.com: Key-Value Observing Done Right

Mikeash 先是吹捧了一下 KVO 机制,非常强大和好用,但是 API 设计非常糟糕:

  1. -addObserver:forKeyPath:options:context: 不支持 selector 参数,对比 NSNotificationCenter 的设计,可谓高下立判, KVO 必须要在 -observeValueForKeyPath:ofObject:change:context: 中处理消息或者传递给父类;
  2. 因为不支持 selector 参数,所以如果在相同的 observer 监听相同的 KeyPath 时,需要通过 context 参数来进行区分;
  3. -removeObserver:forKeyPath: 不支持 context 参数, KVO 是在 iOS2.0 时增加的,后面在 iOS5.0 新增了 -removeObserver:forKeyPath:context: ,支持 context 参数。

KVO Considered Harmful

kvo-considered-harmful

KVO 缺点:

  1. 所有回调都在同一个方法中进行,稍不留意这个方法就会快速膨胀;
  2. 使用字符串硬编码,如果被监听的对象修改了属性名,编译期无法察觉;
  3. 要求处理父类的 KVO 流程;
  4. 移除 observer 时有可能会崩溃;
  5. 充斥着大量有可能会失败的操作,作者认为一个好的 API 设计应该起到使用者成功地调用他们,即使没有解释为什么要这样去调用;
  6. 流程过于隐藏,没办法追踪数据改变的流程,与 delegate 模式相比, KVO 在 debug 时比较麻烦,且需要在运行时通过 isKindOfClass: 动态判断类型;
  7. 有可能造成死循环,如果不小心在回调中修改了监听的属性,那么就会造成死循环,如果说两个属性在不同的 KVO 流程中互相修改,也会造成死循环,且难于 debug ;
  8. KVO 在某些场景下会失效,比如说 __weak 属性,在 __weak 对象被释放时, KVO 是不会去清理对应的监听,导致可能会出现野指针崩溃;
  9. KVO 是一种老旧的模式,在 Apple 平台上,我们可以通过其它方式比如说 Delegate ,Block 和明确的发布/订阅 ( NSNotificationCenter )方式来解决问题,而不是使用 KVO 这种隐晦的方式。

什么时候可以使用 KVO :

  1. Apple 官方要求,比如说 AVPlayer ,要求通过监听 status 属性来获取播放器的状态;
  2. 设计相关的 API 给其他开发者使用。

刨根问题 KVO 原理

刨根问底KVO原理

通过源码相关的伪代码来探究 KVO 的实现方式,如果需要深入了解 KVO 的原理,可以阅读下这篇文章。 KVO 的原理看起来虽然比较简单,但是实现时还是有不少坑,比如说多线程,系统的具体实现也体现了这一点,通过 pthread_mutex_lock 来保证线程安全。

KVOController 解析

draveness/analyze

facebookarchive/KVOController

为了解决 KVO 非常难用的问题,Facebook 开源了 KVOController ,优点如下:

  1. 不需要手动移除 observer ,这里利用了关联属性在对象释放时也会被释放的原理,在关联属性的 dealloc 方法中移除 observer
  2. 支持使用 block ,减少复杂度,添加监听和处理通知的代码可以放在同一处。

基于 KVO hook 子类的方法

一种基于KVO的页面加载,渲染耗时监控方法

在做 ViewController 的耗时检测时,我们需要记录各个 UIViewController 子类对应方法的耗时,如果只是针对 UIViewController 的方法进行 hook ,那么只能记录到 UIViewController 的方法耗时,无法获取子类的方法耗时。

在进行 KVOruntime 实际上会帮你创建一个 KVO 相关的子类,由此可以在初始化时进行一次 KVO 来生成一个新的子类,然后对这个子类方法进行耗时检测。

至于为什么使用 KVO 的方式,下面这篇文章有进行解释,而且也给出了具体实现代码:

巧妙利用KVO实现精准的VC耗时检测

panmingyang2009/VCProfiler

KVO 在不同的二进制中多个符号并存的 Crash 问题

KVO在不同的二进制中多个符号并存的Crash问题

当两个产物都有相同的类名时,比如主二进制和动态库中,这两个类都会被 realize ,都能够被正常调用。

其原因在于苹果使用的是 two level namespace 的技术。在这种形式下,符号所在的“库”的名称也会作为符号的一部分。链接的时候, staic linker 会标记住在这个符号是来自于哪个库的。这样不仅大大减少了dyld搜索符号所需要的时间,也更好对后续库的更新进行了兼容。

但是由于全局类表的存在,在动态创建 KVO 的子类时,只能产生一个。所以就导致 allocate 失败,从而引发 register 过程的 Crash 问题。