ZhouJiatao's Blog

iOS Key-Value Observing(KVO)

Key-value observing(简称KVO)是一种机制,它允许某个对象的属性发生变化时通知其它对象。

Key-value observing主要的好处是,它拥有框架级的支持,使用Key-value observing无需为项目导入其它代码。

不同于NSNotificationCenter的通知,KVO机制中没有负责发送通知的中心对象。当对象的属性发生变化时,该对象直接向所有它的观察者发送通知。

注册Key-Value Observing

为了正常接收到Key-Value Observing通知,必须遵循3个步骤:

  1. 被观察对象调用addObserver:forKeyPath:options:context:方法添加观察者.
  2. 观察对象(接收通知的一方)内部实现 observeValueForKeyPath:ofObject:change:context:方法用于接收通知.
  3. 当观察者不想(或不能)接收通知时,使用removeObserver:forKeyPath:方法注销。至少需要在观察者从内存释放之前调用该方法。

step1:注册为观察者

被观察对象调用addObserver:forKeyPath:options:context:方法添加观察者.
eg: 将self注册为观察者,监听account对象的balanceinterestRate属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Creating context pointers
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
//Registering sef as an Observer
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}

Options

Options参数用于指定接收什么类型的通知、通知需要携带什么内容。

NSKeyValueObservingOptions:

  • NSKeyValueObservingOptionNew: 指明通知中应该包含 属性的新值。
  • NSKeyValueObservingOptionOld: 指明通知中应该包含 属性的旧值。
  • NSKeyValueObservingOptionInitial: 一旦指明此option,被观测对象会立刻发送通知(在addObserver:forKeyPath:options:context:return之前)。通常用于获取属性的初始值。
  • NSKeyValueObservingOptionPrior: 指明在属性每次变化前和变化后都发送通知。并且通知中的change dictionary 总是包含NSKeyValueChangeNotificationIsPriorKey条目,值为NSNumber封装的bool类型,等于@YES表明接收到的是变化前的通知,等于@NO表明接收到的是变化后的通知。

Context

Context参数是一个指针,会传回给观察者的回调方法,作为唯一标识使用。大多数情况下,在回调方法内使用key path字符串就可以区分不同的通知,所以Context参数通常会设置为nil或者NULL

step2:接收通知

观察对象必须实现observeValueForKeyPath:ofObject:change:context:方法。
当被观察属性的值发生变化时,观察对象的observeValueForKeyPath:ofObject:change:context:方法会被回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Implementation of observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}

在本示例中,通过key path足以判断是哪个对象的属性发生了变化,所以可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Implementation of observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqualToString:@"balance"]) {
// Do something with the balance…
} else if ([keyPath isEqualToString:@"interestRate"]) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}

step3:移除观察者

被观察对象调用removeObserver:forKeyPath:context:方法移除观察对象。

1
2
3
4
5
6
7
8
9
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}

常见问题

报错:libc++abi.dylib: terminate_handler unexpectedly threw an exception

发生于被观察者向一个不存在的对象发送通知,所以,一定不要忘记在合适的时候进行 removeObserver 操作,至少在观察者内存被释放之前。
此错误最常发生于,ViewController pop 后或 dismiss 后。

KVO在swift中不管用?

对于自定义的 class,首先需要确保该 class 是 NSObject 的子类;
其次,需要使用dynamy关键字修饰被察者属性;
比如,我们自定义Person类,希望它的birthday属性可以被观察:

1
2
3
class Person: NSObject {
dynamic var birthday = NSDate()
}

适合在ViewController addObserver/removeObserver 的时机?

个人推荐通常情况下在 viewDidAppear 中 addObserver: 此时视图都加载完毕,并显示出来。
在 viewDidDisappear 中 removeObserver:此时视图已经消失,并且视图对象可能已经销毁。
这只是通常情况,你需要根据自己的需求选择最合适的时机。

另一个建议是,不要在 viewWillAppear、viewWillAppear 中 addObserver、removeObserver:
因为一个 ViewController 作为 subcontroller 时,其 viewWillAppear、viewWillAppear并不会在每次视图显示、消失时被调用。

参考链接:
Introduction to Key-Value Observing Programming Guide
Registering for Key-Value Observing
Understanding Key-Value Observing and Coding