Introduction to Key-Value Observing Programming Guide翻译, 找重点可直接跳到第六讲总结,后续细节再进行补充
一、 介绍
Key-value observing 是一种让对象 在其他对象的特定属性改变的时候 被通知到的机制
- 为了理解KVO, 你必须先理解KVC
1.1 概要
对于应用中模型和控制器层之间交流来说,KVO是极其有用的。(在OS-X当中, 控制器层binding技术严重依赖于KVO)。一个控制器对象通常观察模型对象的属性, 一个视图对象通过控制器来观察模型对象的属性。此外,模型对象可能会观察其他模型对象(在一个依赖的值发生改变的时候确定), 或者观察自己
比如Person对象在BankAccount对象发生变化的时候收到通知。也许你让BankAccount的属性暴露在外面让Person时不时去访问, 但是这是没有效率而且不切实际的做法。更好的方式是去使用KVO, 当一笔转账发生的时候让Person收到消息。
对于KVO来说, 你必须确保被观察的对象是兼容KVO的。如果你的对象继承自NSObject, 同时你以常规的方式创建属性, 你的对象和它的属性都是自动兼容KVO的。手动设置KVO兼容也是可以的。
1.2 监听步骤
1.2.1 注册监听
Person对Account发送 addObserver:forKeyPath:options:context:
消息, 对于每个被监听的keypath, 将自己作为监听者
1.2.2 实现方法
为了能够收到Account改变的通知, Person需要实现observeValueForKeyPath:ofObject:change:context:
方法, 所有监听者都需要实现这个方法, 每次注册的keypath对应属性发生变化的时候, Account都会通过这个方法向Person发送消息。Person能够在这个方法里面针对Account的变化采取合适行动
1.2.3 注销通知
一旦不需要通知后, 在至少在观察者对象被销毁之前, Person实例必须通过 removeObserver:forKeyPath:
这个方法对Account发送消息
KVO的好处是你不需要设计实现你自己的一套方案,就能让对象在一个属性改变的时候收到通知。KVO被定义的基础规范有着很好的框架层面支持, 这让它很容易就被采用, 尤其是你不想在你的项目添加太多代码的时候。此外, KVO功能齐全, 能够很容易支持多个观察者,无论是对于一个简单属性还是依赖值都能观察
不像NSNotification使用NSNotificationCenter, KVO当中没有中央对象对所有观察者提供改变的消息。作为代替, 当值发送改变的时候, 消息都是直接发送给观察者。NSObject提供了KVO的基本实现, 你几乎不需要重写这些方法
二、KVO注册 流程
你必须遵循以下步骤来确保一个对象能够接收到KVO兼容的属性发出的KVO消息
- 被观察的对象调用
addObserver:forKeyPath:options:context:
注册观察者 - 观察者实现
observeValueForKeyPath:ofObject:change:context:
来接收消息 - 不再需要接收消息的时候, 通过
removeObserver:forKeyPath:
移除观察者。至少在观察者在内存中释放前调用这个方法
注意: 并非所有的类对所有属性都是KVO兼容的。你可以在第三章查看具体细节确保你自己的类是KVO兼容的。通常Apple提供的框架中的属性会在文档中标识出是KVO兼容的
2.1 注册成为监听者
addObserver:forKeyPath:options:context:
- 参数
- 监听者对象
- 被监听的属性key path
- options和context作为额外的参数去管理通知的各个方面
2.1.1 Options
被指定的按位与的option常量。影响通知提供的描述改变的字典的内容, 以及通知生成的方式
NSKeyValueObservingOptionOld
选择接收到被观察属性在改变之前的值NSKeyValueObservingOptionNew
请求被观察属性的新值NSKeyValueObservingOptionInitial
让被观察的对象立刻发送一个改变的消息, 在addObserver:forKeyPath:options:context:
返回之前。你可以通过这个额外的一次性的通知去确定观察者中的属性的初始值NSKeyValueObservingOptionPrior
让被观察的对象除了在属性被改变之后发送通知外, 在属性改变之前发送一次通知。- 描述改变的字典中包含了
NSKeyValueChangeNotificationIsPriorKey
这个键, 对应的值是NSNumber包裹的YES - 观察者自身可能需要这个改变之前的通知, 去调用
willChange..
方法, 为所有依赖于被观察属性的其他属性做准备。 - 常规改变的消息相较于这种来说太晚了, 无法及时调用
willChange
- 描述改变的字典中包含了
2.1.2 Context
context指针包含了任意的数据, 将会在相应的改变通知中回调给观察者。你可能将它写成NULL, 只依靠key path字符串确定改变通知的来源。如果一个对象的父类出于不同的原因也观察了同样的key path,那么传NULL就会出问题。
更加安全和可扩展的方式是使用context确保你收到的通知是针对当前对象而不是父类。
在你的类当中一个命名唯一的静态变量的地址可以作为很好的context。选择相似的命名方式会让context不容易被父类或者子类覆盖掉。你可以为整个类选一个context, 根据通知中的key path string决定改变哪些内容。你也可以为每一个key path创建唯一的context,来绕过key path 字符串比较, 让通知解析地更加有效率
// 创建context指针
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
// 注册观察者
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
注意: 这个方法不会对观察者、被观察者、上下文产生强引用关系, 你应该确保你自己对观察和被观察者维持了强引用关系, 有必要的话上下文也一样
2.2 接收到改变的通知
当一个对象被观察的属性改变的时候, 观察者收到observeValueForKeyPath:ofObject:change:context:
的消息。所有观察者都必须实现这个方法。
-
观察对象提供
- 触发通知的key path
- 本身作为关联对象
- 提供一个包含了改变信息的字典
- 观察者注册的时候提供的context的指针
-
改变字典包括
NSKeyValueChangeKindKey
发生改变的类型NSKeyValueChangeSetting
被观察的对象已经发生改变- 一对多关系属性
NSKeyValueChangeInsertion
插入NSKeyValueChangeRemoval
移除NSKeyValueChangeReplacement
替代
NSKeyValueChangeOldKey
需要注册时声明对应选项 属性改变前的值- 如果改变的是一个对象, 键对应的也是一个对象
- 如果改变的是基本数据类型或者结构体, 键对应的是NSValue
- 如果改变的是一个集合类型, 返回一个数组, 数组包含改变之前的对象
NSKeyValueChangeNewKey
需要注册时声明对应选项 属性改变后的值- 同上
NSKeyValueChangeIndexesKey
一对多关系对象中改变的index, 是NSIndexSet对象
- (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];
}
}
- 如果context注册的时候填的NULL, 通过比较通知中的key path确定什么发生了改变
- 如果所有key path都使用了同一的context,尝试先匹配context,之后通过key path字符串确定什么发送了变化(确定是当前类收到通知而不是父类)
- 如果你对每个key path提供了唯一的context,通过比较context, 你能够同时确定是当前观察者收到了通知并且能够确定是观察的哪个属性发生了变化
- 不论是哪种方式, 在无法识别的情况下, 都应该调用父类的方法, 代表父类也注册成为了观察者收到了通知
注意:如果通知传播到了类的继承链的顶部, NSObject抛出异常NSInternalInconsistencyException
表示子类无法消化掉注册的一个通知
2.3 移除观察者
收到removeObserver:forKeyPath: context:
消息后, 观察对象不再能够接收对于特定的key path和对象的任何observeValueForKeyPath:ofObject:change:context:
消息
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
- 当移除观察者的时候, 记住以下几点
- 没有注册成为观察者, 但却要求移除该对象,
NSRangeException
- 一次注册对应一次移除
- 出现问题尝试用 try catch包裹
- 一个观察者在dealloc的时候并不会自动移除。被观察者依然会发送消息,无视观察者的状态是否是被销毁。给一个已经释放的对象发送消息会触发memory access exception。所以你应该确保对象释放内存之前就移除观察者的身份
- 协议里没有提供方法判断是否一个对象是观察者或被观察者。好好构造你的代码避免和释放相关的错误。一个典型的做法就是在一个对象初始化的时候注册成为监听者(init和viewDidLoad), 然后在释放的时候注销(dealloc),确保配对和有序添加移除以及观察者在从内存释放之前已经是注销状态。
- 没有注册成为观察者, 但却要求移除该对象,
三、KVO兼容
- 为了对于某一特定的属性兼容KVO, 一个class必须确保以下条件
- 这个类对于这个属性来说必须是兼容KVC的。 KVO支持和KVC相同的数据类型, 包括OC对象、基本数据类型和结构体
- 这个类会为属性发出KVO变化消息
- 依赖的key是使用合适的方式注册的
存在两种方式确保改变的通知被发送出去。自动的是NSObject提供的, 默认对类下的所有兼容KVO的属性都是可行的。如果你遵循标准的Cocoa编码和命名惯例,你能够使用自动通知, 不需要写额外的代码。手动通知提供额外的控制, 也需要额外编码。你可以控制子类的自动通知, 通过重写类方法automaticallyNotifiesObserversForKey:
3.1 自动的变化通知
NSObject提供了自动key-value变化通知。自动的变化通知会在使用KVC访问器或者KVC方法的时候触发。返回的集合代理对象也只支持自动通知
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
3.2 手动的变化通知
在某些场合, 你可能希望控制通知的进展,比如最小化地触发对于应用程序来说不必要的通知, 比如将多个变化整合进一个通知, 手动变化通知就提供了这样的方式。
手动通知和自动通知并不互斥, 在已经存在自动通知的情况下, 你也可以发送手动通知。比如, 你希望完全掌控某一个属性的通知。这种情况下, 你重写NSObject的automaticallyNotifiesObserversForKey:
。如果希望排除自动通知, 子类的这个方法应该返回NO。 对于未识别出的key应该调用父类的方法
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
为了实现手动通知, 在改变值之前调用 willChangeValueForKey:
, 在改变值之后调用, didChangeValueForKey:
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
你可以最小化不必要的通知, 通过检查值是否发生变化
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
如果一个操作造成多个值变化, 你应该嵌套起来
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
对于有序的一对多关系, 你不单单要指出改变的key, 也要指出改变的类型和涉及的下标。
- 改变的类型是
NSKeyValueChange
NSKeyValueChangeInsertion
NSKeyValueChangeRemoval
NSKeyValueChangeReplacement
- 下标存在NSIndexSet对象里
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}
四、注册依赖键
存在很多一个属性依赖于一个或者多个其他对象的属性的场景。如果一个属性发生变化, 衍生的属性也应被标记为需要改变。如何确保KVO通知发送给这些有依赖的属性, 这取决于关系的基数
4.1 1对1关系
4.2 1对多关系
五、KVO实现细节
自动的KVO是使用isa-swizzling(类指针交换)的技术
isa指针指向实例对象的类对象, 类对象里面有一个调度表。表中本质上包含了指向类的方法实现的指针
当一个观察者注册了一个对象的属性时, 被观察对象的isa指针会被修改, 指向一个由原本类派生出来的类。结果就是isa指针的值不必指向实例真正的类
你不应该通过isa指向来判断对象属于哪个类, 你应该使用class
方法来确定
六、总结
6.1 KVO实现原理
使用了isa-swizzling(技术), 在运行时动态生成一个NSKVONotifying_的派生类, 这个类重写了set方法, set方法在改变之前调用了willChangeValueForKey:
, 改变之后调用了didChangeValueForKey:
, 通过set方法观察的属性发生变化的时候,didChangeValueForKey:
会让被观察者向观察者发送observeValueForKeyPath:ofObject:change:context:
消息。
注册观察者时让isa指针指向这个类, 在移除观察者时指回原来的类。
6.2 如何触发KVO
- 调用set设置属性自动触发
- 调用KVC设置属性自动触发 需要有对应set方法
- 手动调用, 改变值之前调用
willChangeValueForKey:
, 在改变值之后调用,didChangeValueForKey:
6.3 如何关闭KVO的自动通知
重写automaticallyNotifiesObserversForKey:
方法, 对于特定键返回NO, 其他调用父类的实现
6.4 KVO NSNotification delegate区别
这几种方式就监听某个属性的变化而言
- KVO
- 一对多 一个被观察者可以向多个观察者发送消息
- 被观察者可以不用显示操作
- 单向传值
- 对象必须是支持KVC的 (继承自NSObject有默认的实现, 或者自己实现了NSKeyValueCoding)
- NSNotification
- 一对多
- 监听范围很广
- 单向传值
- 被观察者主动发通知, 相较于KVO来说是显式的
- 适合于视图层级过多, 不便于传递事件的时候
- delegate
- 一对一
- 正反向传值, 执行协议的方法以参数的方式传给delegate, delegate执行完方法后以返回值的方式给到delegate持有者
- delegate有必选方法和可选方法