Introduction
Key-value observing
(KVO) 是一种机制,它允许对象在 更改其他对象的指定属性时 得到通知。
At a Glance
在应用中在 model
和 controller
层间通讯非常有用(在 OS X中, controller
层 binding
技术严重依赖 KVO), controller
同时观察 model
的属性, view
通过 controller
观察 model
的属性. 另外, model
可能观察其他 model
甚至是 model
本身(通常由依赖值决定).
可以观察包括简单的属性, to-one
and to-many
关系. to-many
的观察者被通知 change 的类型就像改变涉及的值一样.
简单例子, 假设 Person
有一个 Account
属性, 表现为 person
保存 account
到银行. Person
实例可能需要知道 Account
实例的某些方面何时发生更改,比如余额或利率。
如果 Account
的属性时公有属性, 则 Person
可以通过轮询查看 changes
, 这种方式显然低效, 更好的方法是使用 KVO, 在 change
时, 类似 Person
接收一个分岔.
使用 KVO, 必须确定被观察对象, Account
是否符合. 通常, 如果对象继承 NSObject
, 使用一般方式创建的属性, 都自动符合 KVO, 也可以手动实现遵从性. KVO Compliance 描述了自动和手动 KVO 以及怎样实现.
下一步, 必须注册观察者实例, 例如 Person
, 被观察者 Account
, Person
发送一个 addObserver:forKeyPath:options:context:
消息给 Account
, 为每个被观察的 key-path
,并将自己命名为观察者。
为了接收 Account
改变的消息, Person
实现 observeValueForKeyPath:ofObject:change:context:
方法, 每个观察者都必须实现. Account
在注册的 key-path
发生改变时发送消息给 Person
. Person
作相应处理
最终, 当它不想收到消息, 至少要在它析构之前 Person
实例必须通过发送 removeObserver:forKeyPath:
消息给 Account
注销.
Registering for Key-Value Observing 描述了 KVO 从注册到注销的生命周期.
KVO 主要好处就是不需要实现自己的方案来在每次属性更改时发送通知. 具有良好的基础设置具有框架级的支持, 使其便于采用--通常你不需要想工程中添加任何代码. 此外,基础设施已经功能齐全,这使得支持单个属性的多个观察者以及相关值变得很容易。
Registering Dependent Keys 解释了如何定义一个 key 依赖另一个 key.
不像 NSNotificationCenter
, 它没有中心对象给所有观察者提供 change 消息. 当发生 change 时,消息直接被发送给观察的对象, 这是 NSObject 提供 KVO 的基本实现.
Key-Value Observing Implementation Details KVO 是如何实现的.
Registering for Key-Value Observing
必须执行以下步骤以让一个对象可以接收遵循 KVO 属性的 KVO 消息:
- 用
addObserver:forKeyPath:options:context:
方法给被观察者注册观察者. - 在观察者内部实现
observeValueForKeyPath:ofObject:change:context:
方法接收通知消息. - 使用
removeObserver:forKeyPath:
注销观察者当它不想收到消息使, 最低限度, 在观察者从内存被释放之前调用.
并非所有类的属性都符合 KVO. KVO Compliance中描述如何确定类是否符合 KVO, 通常苹果提供的 frameworks 中的属性只有在被文档化时才符合 KVO.
Registering as an Observer
addObserver:forKeyPath:options:context:
:
Options
选项参数指定为位或选项常量,它影响通知中提供的更改字典的内容,以及生成通知的方式。
NSKeyValueObservingOptionOld
(change 前旧值) |
NSKeyValueObservingOptionNew
(change 后新值) |
NSKeyValueObservingOptionInitial
(addObserver:forKeyPath:options:context:
方法返回前) |
NSKeyValueObservingOptionPrior
(在属性 change 之前的通知, 普通通知在其后, 通过NSKeyValueChangeNotificationIsPriorKey
获取值为NSNumber
转换为 YES, 当观察者自身的 KVO遵从性要求它为其依赖于所观察属性的属性调用-willChange…
之一时, 一般的通知来得太晚, 无法及时调用willChange...
)Context
context
指针会在观察者对应的消息中传递. 可以指定为NULL
也可以完全依赖于keypath
字符串决定通知的初始化. 但是这会导致对象父类(也实现了一个相同keypath
的观察)一些不明原因的问题.
一个更安全可扩展的方法就是通过context
保证通知是你指定的观察者而不是父类.
类中唯一命名的静态变量的地址是一个很好的上下文。可以为整个类选择上下文, 并依赖keypath
:
static void * PersonAccountBalanceContext =&PersonAccountBalanceContext;
static void * PersonAccountInterestRateContext =&PersonAccountInterestRateContext;
将自身注册为 Account
实例 balance
和 interestRate
属性的观察者:
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
addObserver:forKeyPath:options:context:
方法不保持对观察者,被观察对象或上下文的强引用。您应该确保在必要时保持对观察者,被观察对象和上下文的强引用。
Receiving Notification of a Change
当对象的属性发生变化时, 观察者会接收到 observeValueForKeyPath:ofObject:change:context:
消息.
change 字典 key NSKeyValueChangeKindKey
包含发生改变的信息. 如果被观察的值发生变化, NSKeyValueChangeKindKey
返回 NSKeyValueChangeSetting
. NSKeyValueChangeOldKey
and NSKeyValueChangeNewKey
包含属性的变化的前后值. 如果属性是一个对象, 值直接被提供, 如果是一个标量或一个 C 结构体, 会转换为 NSValue
对象(as KVC).
如果被观察对象是一个 to-many 的关系, NSKeyValueChangeKindKey
会标识是NSKeyValueChangeInsertion
(插入), NSKeyValueChangeRemoval
(删除), or NSKeyValueChangeReplacement
(替换).
NSKeyValueChangeIndexesKey
是一个 NSIndexSet 对象指定变化的关系索引.
如果注册观察者时指定 NSKeyValueObservingOptionNew
or NSKeyValueObservingOptionOld
options, 则 change 字典中 NSKeyValueChangeOldKey
and NSKeyValueChangeNewKey
返回关联对象改变前/后 的值数组.
- (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];
}
}
注意: 如果通知传播到类层次结构的顶部,则
NSObject
抛出一个NSInternalInconsistencyException
因为这是一个编程错误:子类无法使用它注册的通知。
Removing an Object as an Observer
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
移除观察者记住以下几点:
- 移除一个未注册的观察者会导致
NSRangeException
. 只需要为对应于addObserver:forKeyPath:options:context:
调用一次removeObserver:forKeyPath:context:
, 如果不可行, 则可以将removeObserver:forKeyPath:context:
放到try
/catch block
中处理潜在的异常。 - 当观察者析构时不会自动移除. 被观察对象继续发送消息, 像发送一个消息给已经释放的对象, 触发 exception.
- 无法判断一个对象是否是观察者或被观察对象. 避免这种情况. 典型的方式是在观察者初始化的时候注册(
init
orviewDidLoad
), 在对象析构时注销(dealloc
), 确保正确配对和排序添加和删除消息, 观察者在释放之前注销.
KVO Compliance
遵循 KVO, 类必须确保:
- 指定的属性遵循 KVC, KVO 同 KVC 一样支持相同的数据类型.
- 由类为属性变化通知.
- 相关 key 已正确注册.
NSObject 默认自动支持类遵循 KVC 的属性.
子类通过实现 automaticallyNotifiesObserversForKey:
方法控制是否自动发消息.
Automatic Change Notification
NSObject 提供自动键值更改通知的基本实现:
//调用访问器方法。
[account setName:@"Savings"];
//使用 setValue:forKey:。
[account setValue:@"Savings" forKey:@"name"];
//使用 key-path,其中'account'是'document' 遵循 KVC 的属性。
[document setValue:@"Savings" forKeyPath:@"account.name"];
//使用 mutableArrayValueForKey: 检索 关系代理对象。
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
Manual Change Notification
手动通知, 控制指定的 key 发送 change 通知.
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
手动通知需要在属性值发生改变前后分别调用 willChangeValueForKey:
和 didChangeValueForKey:
:
手动调用 balance 属性的 KVO.
- (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
值, 索引是 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"];
}
Registering Dependent Keys
在许多情况下,一个属性的值取决于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生了变化,那么派生属性的值也应该标记为变化。如何确保为这些依赖属性发布 KVO 通知取决于关系的基数。
To-One Relationships
触发 to-one 关系对象自动发出消息, 需要重写 keyPathsForValuesAffectingValueForKey:
方法或适合的方法,该方法遵循它为注册依赖键定义的模式。
例如, fullName
依赖 first
and last name
, 获取 fullName
的方法可能为:
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
观察 fullName
的对象, 不管 firstName
还是 lastName
属性发生改变时都应该被通知到, 作为他们影响的值.
一个解决办法是通过重写 keyPathsForValuesAffectingValueForKey:
方法指定 fullName
属性依赖 firstName
和 lastName
属性:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
也可以实现一个形如 keyPathForValuesAffecting<Key>
的类方法:
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
不能通过分类形式给一个已经存在的类重写 keyPathsForValuesAffectingValueForKey:
方法, 但可以实现 keyPathsForValuesAffecting<Key>
类方法.
To-Many Relationships
keyPathsForValuesAffectingValueForKey:
方法不支持 to-many
关系对象, 比如有一个 Department
对象有一个 to-many
关系对象(employees
)包含 Employee
, Employee
有一个 salary 属性. 你可能想要 Department
对象有一个 totalSalary
属性统计所有 Employees
的 salaries
. 使用 keyPathsForValuesAffectingTotalSalary
返回 employees.salary
是不行的.
有两种解决方法:
-
可以通过注册父类对象(
Department
)作为观察者, 观察所有子属性(Employees
). 在关系对象中(employees
)中添加或移除子对象时, 必须响应的添加和移除父对象观察者. 在observeValueForKeyPath:ofObject:change:context:
方法中根据 changes 更新依赖值:- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == totalSalaryContext) { [self updateTotalSalary]; } else // deal with other observations and/or invoke super... } - (void)updateTotalSalary { [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]]; } - (void)setTotalSalary:(NSNumber *)newTotalSalary { if (totalSalary != newTotalSalary) { [self willChangeValueForKey:@"totalSalary"]; _totalSalary = newTotalSalary; [self didChangeValueForKey:@"totalSalary"]; } } - (NSNumber *)totalSalary { return _totalSalary; }
如果是
Core Data
, 则可以通过NSNotificationCenter
注册通知.
Key-Value Observing Implementation Details (KVO 的实现细节)
KVO 的实现使用的是 isa-swizzling
技术.
isa 指向维护在 dispatch table
中的类的对象. dispatch table
中包含类实现的方法以及其他数据.
当一个对象的属性注册了一个观察者, 被观察对象的 isa 会被修改, 指向一个中间类而不是真正的类. 因此,isa 指针的值不一定反映实例的实际类。
永远不要使用 isa 来判断类的从属关系. 应该使用 class 方法.