Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
KVO 提供了一种对象监听机制,当被观察对象发生改变的时候,观察者会收到通知,但是使用过 KVO 的朋友都知道,我们通常并不需要在被观察对象里边写任何代码。既然这样,KVO 是怎样监听属性的呢?
KVO 原理
还是先来看下官方文档怎么说:
Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
苹果透露 KVO 的实现使用了 isa-swizzling 技术。
当我们使用 KVO 去观察一个对象的时候,一个新的类会在运行时创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,去通知所有的观察者。然后通过 isa-swizzling 把这个被观察对象的** isa 指针指向新创建的子类**,于是,被观察对象就这样变成了新创建的子类的实例了。
当一个属性的值发生改变的时候,setter 方法会被调用,所以,去通知观察者的这件事情发生在属性的 setter 方法里。事实上,键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:
和 didChangevlueForKey:
. 在一个被观察属性发生改变之前, willChangeValueForKey:
首先会被调用,该方法负责记录旧的值。而当改变发生后, observeValueForKey:ofObject:change:context:
会被调用,最后调用 didChangeValueForKey:
.
伪码表示:
- (void)setFoo:(id)foo
{
[self willChangeValueForKey];
_foo = foo;
[self observeValueForKey:key ofObject:target change:CHANGE context:nil];
[self didChangeValueForKey];
}
这也意味着通过“直接访问”(比如 _foo 而不是 self.foo)的方式去设置属性值是不会触发 KVO 的。
此外,除了 setter 被重写外,苹果还重写了 -class ,让其返回原来所属的那个类,比如说 [ _foo class ] 返回的是原来所属的类而不是新创建的子类。
我们还可以利用运行时去深入挖掘 KVO 的底层实现,这里推荐阅读 Mike Ash 的这篇文章。
KVO 缺陷
关于 KVO 的一直存在很多的争议,详见NSHipster、KVO Considered Harmful 和 Key-Value Observing Done Right.
总结一下 KVO 的缺陷主要有以下几个方面:
- API 设计缺陷。NSNotification 的 API 允许我们传入 selector( -addObserver: selector: name: object: ),这样收到通知的时候就可以调用到 selector ;相反使用 KVO 的话,我们必须在观察者类当中重写 -observeValueForKeyPath:ofObject:change:context: 方法,我们不得不在这个方法里加很多判断逻辑写一坨代码来监听不同的属性。
- 在 -addObserver:forKeyPath:options:context: 里传入 NSString 对象作为 keyPath,编译器是无法检测错误的,如果属性名被更改了,这个观察就没意义了。这个问题可以通过 NSStringFromSelector 稍稍改善。另外,传给 context 的参数大部分情况下都是 nil.
- 如果父类和子类都监听了同一个对象的同一个属性,很容易出现父类中remove了一次,子类又remove了一次的情况,应用程序第二次 remove 就会抛出异常然后 crash. 解决方案见 KVO Considered Harmful .
- KVO 可能会导致死循环。死循环发生的场景是:你在-observeValueForKeyPath 方法里边设置了属性,而这个属性刚好被自己监听。
- 某些情况下使用 KVO 监听属性会失效。比如说被监听属性的 setter 被 hook 了;还有一种情况是,对 weak 属性监听,如果 weak 实例变量指向的对象被释放了,weak 修饰的实例变量会自动设为 nil,KVO 也就失效了。
坑很多,KVO 使用的时候要特别地小心。
KVO 手动实现
KVO 使用的时候要注意的地方有很多,嫌麻烦我们可以自己撸一个玩。
实现思路可以去看 Glow 团队的这篇文章,我就不搬砖了。基于类似的思路,我实现了一个能够传入 selector 参数的 KVO. Demo 放在我的 github 上。
参考文章:
How Key-Value Observing is actually implemented at the runtime level
如何自己动手实现 KVO
KVO Considered Harmful
Key Value Observing
Key-Value Observing Done Right
Creating Classes at Runtime in Objective-C
Associated Objects