第七部分,KVO。

键值观察(Key-Value Observing),或者叫 NSKeyValueObserving,是一个非正式协议,NSObject 默认实现该协议。KVO 是基于 KVC 以及动态派发技术实现的,它定义了对象之间观察和通知状态改变的通用机制。

Classes 可以选择自动退出 KVO 通过复写:
+automaticallyNotifiesObserversForKey: 并且返回 No。

KVO 利用 @try / @catch 安全的取消注册

如果调用 –removeObserver:forKeyPath:context:,但是这个对象没有被注册为观察者(已经解除注册或者还没有开始注册),就会抛出一个异常。

没有一个内建的方式来检查对象是否注册,这就导致我们需要用一种相当不好的方式 @try 和一个没有处理的 @catch。没有处理一个捕获的异常,这是一种妥协的方式。因此,只有当面对连续不断的崩溃并且不能通过一般的措施(竞争条件或者来自父类的非法行为)补救才会用这种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqualToString:NSStringFromSelector(@selector(isFinished))]) {
if ([object isFinished]) {
@try {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(isFinished))];
}
@catch (NSException * __unused exception) {}
}
}
}

KVO 和线程

一个需要注意的地方是,KVO 行为是同步的,并且发生与所观察的值发生变化在同样的线程上。没有队列或者 Runloop 的处理。手动或者自动调用 -didChangeValueForKey: 会触发 KVO 通知。

所以,当我们试图从其他线程改变属性的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把 KVO 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们之间用 KVO。

KVO 是同步运行的这个特性非常强大,只要我们在单一线程上面(比如主队列 main queue),KVO 会保证下列两种情况的发生:

首先,如果我们调用一个支持 KVO 的 setter 方法:

1
self.exchangeRate = 2.345;

KVO 能保证所有 exchangeRate 的观察者在 setter 方法返回前被通知到。

其次,如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 直到 -observeValueForKeyPath... 被调用之前,exchangeRate 的 accessor 方法都会返回同样的值。

KVO 实现

用一句话表述,Apple 使用了 isa 混写(isa-swizzling)来实现 KVO。

当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。自然,重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象值得更改。最后把这个对象的 isa 指针指向这个新创建的子类(isa-swizzling),对象就神奇的变成了新创建的子类的实例。

原来,这个中间类,继承自原本的那个类。不仅如此,Apple 还重写了 -class 方法,企图欺骗我们这个类没有变,就是原来那个类。

KVO 底层实现,例 Person 类:

    1. 动态创建 NSKVONotifying_Person(Person 子类)做 KVO
    1. 修改当前对象的 isa 指针,从 Person -> NSKVONotifying_Person
    1. 只要调用对象的 set 方法,就会调用 NSKVONotifying_Person 的 set 方法
    1. 重写 NSKVONotifying_Person 的 set 方法
      • 4.1 [super set:]
      • 4.2 通知观察者,告诉你属性改变

具体信息,参考:

Reference