KVO键-值观察编程指南
注册键-值观察
为了接收某个属性的键-值观察通知,以下三个要素是必须的:
- 被观察的类当中你关心的属性必须是遵循键-值观察的。
- 你必须使用以下方法,将观察方对象与被观察方对象注册:
:forKeyPath:options:context:
. - 观察方的对象必须实现以下方法:
observeValueForKeyPath:ofObject:change:context:
.
注册为观察者
为了正确接收属性的变更通知,观察对象必须首先发送一个addObserver:forKeyPath:options:context:
消息至被观察对象,用以传送观察对象和需要观察的属性的关键路径,以便与其注册。选项参数指定了发送变更通知时提供给观察者的信息。使用NSKeyValueObservingOptionOld
选项可以将初始对象值以变更字典中的一个项的形式提供给观察者。指定NSKeyValueObservingOptionNew
选项可以将新的值以一个项的形式添加至变更字典。你可以使用逐位OR
这两个常量来指定接收上述两种类型的值。
例子演示了为属性openingBalance
注册一个检查器对象的方法。
将检查器注册为openingBalance属性的观察者
- (void)registerAsObserver
{
// register "inspector" to receive change notifications
// for the "openingBalance" property of the "account" object
// and that both the old and new values of "openingBalance"
// should be provided to the observer
[account addObserver:inspector
forKeyPath:@"openingBalance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:NULL];
}
当你注册一个对象为观察者时,还可以同时指定一个内容指针。当observeValueForKeyPath:ofObject:change:context:
被调用时,内容指针会被提交至观察者。该指针可为C语言指针或对象引用。该指针可作为指定被观察变更的唯一标示符,或是为观察者提供其他数据。该指针不会被保留,应用程序本身应保证该指针在观察方被取消观察者身份之前被释放。
****键-值观察方法addObserver:forKeyPath:options:context:
不会保留观察和被观察对象。你需要检查你的程序需求并管理观察和被观察对象的保留及释放。
接收变更通知
当对象的一个被观察属性发生变动时,观察者收到一个observeValueForKeyPath:ofObject:change:context:
消息。所有观察者都必须实现这一方法。触发观察通知的对象和键路径、包含变更细节的字典,以及观察者注册时提交的上下文指针均被提交给观察者。
变更字典项NSKeyValueChangeKindKey
供发生变更的类型信息。如果被观察对象的值改变了,NSKeyValueChangeKindKey
项将返回一个NSKeyValueChangeSetting
。依赖观察者注册时指定的选项,字典中的NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
项分别包含了被观察属性变更前后的值。
如果被观察的属性是一个对多的关系,NSKeyValueChangeKindKey
项同样可以通过返回三个不同的项NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
来分别指明关系中的对象被执行的插入、删除和替换操作。
变更字典项NSKeyValueChangeIndexesKey
是一个指定关系表变更索引的NSIndexSet
对象。注册观察者时,如果NSKeyValueObservingOptionNew
或NSKeyValueObservingOptionOld
被指定为选项,变更字典中NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
两个项将以数组形式包含相关对象在变更前后的值。
表2中的例子演示了observeValueForKeyPath:ofObject:change:context:
方法实现了在检查器中反映openingBalance
属性的旧值和新值,该属性在上面已被注册。
observeValueForKeyPath:ofObject:change:context:的实现
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:@"openingBalance"]) {
[openingBalanceInspectorField setObjectValue:
[change objectForKey:NSKeyValueChangeNewKey]];
}
// be sure to call the super implementation
// if the superclass implements it
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
移除对象的观察者身份
你可以发送一条指定观察方对象和键路径的removeObserver:forKeyPath:消息至被观察的对象,来移除一个键-值观察者。表3中的例子将检查器移除了其针对openingBalance的观察者身份。
- (void)unregisterForChangeNotification
{
[observedObject removeObserver:inspector
forKeyPath:@"openingBalance"];
}
如果在观察者被注册时,内容被指定为一个对象,那么在观察者被移除后它可以被安全地释放。当接收到removeObserver:forKeyPath:消息后,观察方对象不会再收到关于指定的关键值路径和对象的任何 observeValueForKeyPath:ofObject:change:context:消息。
自动支持和手动支持的对比
自动的键-值观察
NSObject提供了基本的自动进行键-值观察的实现方法。使用自动观察通知可在通过键-值编程和遵循键-值编程方法实现功能时免去对变更的归类,进而无须对willChangeValueForKey:和didChangeValueForKey: 进行请求调用。自动观察者通知受类方法automaticallyNotifiesObserversForKey:控制。默认的实现方法对所有的键值都返回YES。
和键-值编码方法一样,自动的键-值观察将遵循键-值的访问器作出的变更通知给观察者。表1中的例子可实现当属性name发生变更时,其所有观察者都收到变更通知。
// calling the accessor method
[self setName:@"Savings"];
// using setValue:forKey:
[self setValue:@"Savings" forKey:@"name"];
// using a key path, where account is a kvc-compliant property
// of "document"
[document setValue:@"Savings" forKeyPath:@"account.name"]
自动通知还支持mutableArrayValueForKey:和mutableSetValueForKey:返回集合代理对象。这个功能可用于支持insertObject:in<Key>AtIndex:,replaceObjectIn<Key>AtIndex:和removeObjectFrom<Key>AtIndex:等索引存取方法的对多关系。
你可以通过实现类方法automaticallyNotifiesObserversForKey:来控制你的子类的自动观察通知 。子类可以检测参数检测的键值,并在自动通知可用时返回YES ,禁用时则返回NO。
手动观察者通知
手动键-值观察通知针对通知发送至观察者的时间和方式提供更为精确的控制。这可以有效减少无用的触发通知,或是将一组变更集成至一个单独的通知中。
实现手动观察通知的类必须重新实现NSObject类中的automaticallyNotifiesObserversForKey:方法。在同一个类中同时使用自动和手动观察者通知是可行的。对于执行手动观察者通知的属性来说,子类中automaticallyNotifiesObserversForKey:实现应当返回NO。子类实现中对于未在子类中不能识别的键值,必须调用Super。下面的例子对 openingBalance属性启用了手动通知,允许父类确定其它所有键的通知。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"openingBalance"]) {
automatic=NO;
} else {
automatic=[super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
为了实现手动观察通知,你必须在改变值之前调用willChangeValueForKey:并在更改它之后调用didChangeValueForKey:。下面的例子为实现了属性openingBalance的手动观察者通知。
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
openingBalance=theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
你可以先确认值是否发生了变更,以尽量减小发送无用通知的数量。表4中的例子测试了openingBalance
的值,并且只在该值改变时发送通知。
- (void)setOpeningBalance:(double)theBalance {
if (theBalance != openingBalance) {
[self willChangeValueForKey:@"openingBalance"];
openingBalance=theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
}
如果某个单一的操作造成了多个键变动,你必须将所有变更通知整合到一起,如下所示。
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
[self willChangeValueForKey:@"itemChanged"];
openingBalance=theBalance;
itemChanged=itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"openingBalance"];
}
在对多关系情况下,你不仅需要指定被改动的key,同样地还需要指定变更类型和涉及到的对象索引。变更类型是指定NSKeyValueChangeInsertion,SKeyValueChangeRemoval或 NSKeyValueChangeReplacement的NSKeyValueChange。受影响对象的索引将以NSIndexSet形式传递。
下面的代码段描述了在对多关系transactions中内嵌对象删除的方法。
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
// remove the transaction objects at the specified indexes here
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}
请注意,在发送willChange消息前,不要释放将发生改动的值。