KVO介绍
KVO允许在对象的指定属性发生变化时获取通知。这是非常有用的对于模型和控制器层的通讯。控制器对象观察模型的属性,视图对象通过控制器观察模型的属性。除外,模型对象可以观察其他模型对象,甚至是自己。
你可以观察的属性包括简单属性(attributes),一对一关系,一对多关系。观察一对多关系的对象会获取包含属性变化类型和触发对象的通知。
注册KVO
你必须执行下面步骤来激活对象获取指定属性的KVO通知:
• 使用被观察者的addObserver:forKeyPath:options:context:方法来注册观察者。
• 观察者实现observeValueForKeyPath:ofObject:change:context:方法来接收相关通知。
• 当不再接收通知时,调用被观察者的removeObserver:forKeyPath:方法来移除观察者。调用之前观察者没有被释放内存。
注册成为观察者
成为观察者的第一步是向被观察者调用addObserver:forKeyPath:options:context:方法来注册成为观察者。这个方法需要提供观察者对象和需要观察的属性键路径,你还可以提供options可选参数和context上下文指针。
Options
这个options参数是一个按位可选常量,它影响通知的变化字典内容和通知行为。
你可以指定NSKeyValueObservingOptionOld可选常量来获取观察属性改变前的值。你也可以指定NSKeyValueObservingOptionNew来获取属性改变后的值。你也可以通过位或这些可选常量来同时获取这两个值。
你可以指定NSKeyValueObservingOptionInitial来立即获取被观察者对象的属性变化通知(在addObserver:forKeyPath:options:context:前的状态)。你可以在观察者那里使用这个额外的,一次性的初始化属性值。
你可以指定NSKeyValueObservingOptionPrior来获取属性之前变化的通知(不同于在属性变化后才发送的普通通知)。通知的改变字典会包含NSKeyValueChangeNotificationIsPriorKey的键以及对应的YES的NSNumber值。这个键在其他情况是不会出现的。当你需要触发被观察者的willChange…方法,这个方法是对应观察者的某个依赖被观察者属性值的属性,你可以使用这个属性变化前通知来处理这种情况。通常,变化前通知可能来得太晚而不能触发willChange…方法。
Context
addObserver:forKeyPath:options:context:方法的context指针是一个包含任意数据的指针,这个指针可以在观察者对应的变化通知内获取。你可以传递NULL并完全依赖键路径字符串来决定变化通知的来源。但是这种方法可能会造成一些问题,例如对于观察者的父类同样观察同一对象的键路径属性值。
一个安全、可扩展的方式是使用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];
}
注意:addObserver:forKeyPath:options:context: 方法不会强引用观察者和被观察者以及上下文指针。
接收变化通知
当被观察的属性值发生变化,观察者会收到observeValueForKeyPath:ofObject:change:context:通知。所有观察者必须实现这个方法。
通知提供触发的键路径、被观察者、变化字典、context上下文。
变化字典提供NSKeyValueChangeKindKey来获取变化类型。如果被观察对象的属性值改变,这个NSKeyValueChangeKindKey返回NSKeyValueChangeSetting。根据注册观察者的options值,NSKeyValueChangeOldKey和NSKeyValueChangeNewKey返回属性变化前和变化后的值。如果属性值是对象,那么直接返回。如果属性值是数值或者是C结构体,这个值会包装成NSValue对象。
如果观察的属性是一对多关系。NSKeyValueChangeKindKey会返回NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement来指示一对多属性变化是否是插入、删除、替换。
变化字典提供NSKeyValueChangeIndexesKey来获取一对多属性(有下标关系的)的下标变化值(NSIndexSet)。NSKeyValueChangeOldKey和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];
}
}
移除观察者
你通过调用被观察者的removeObserver:forKeyPath:context:方法来移除观察者。
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
在收到removeObserver:forKeyPath:context:方法后,观察者将不会收到指定键路径的observeValueForKeyPath:ofObject:change:context:通知。
当你要移除观察者,你需要留意几点:
• 如果要移除的观察者没有在被观察者中注册,系统会抛出NSRangeException异常。你应该调用一次removeObserver:forKeyPath:context:方法,对应addObserver:forKeyPath:options:context:的调用,或者这不太可能实现,你可以在try/catch中调用removeObserver:forKeyPath:context:方法并处理潜在的异常。
• 观察者不会在被释放内存前自动移除自己。被观察者会无视观察者的状态并继续发送通知。但是,这个变化通知像其他消息一样,发送给已释放的对象会触发内存访问异常。因此你要确保观察者在释放内存前移除它。
• KVO没有提供方法来确定对象是观察者还是被观察者。构建你的代码来避免造成相关错误。一个通用的模式是在观察者初始化前注册为观察者(init或者viewDidLoad),在释放内存前移除观察者(dealloc),确保属性配对并按添加顺序来移除观察者并确保它没有被释放内存。
服从KVO
为了确认指定的属性符合KVO机制,这个类确保符合下面的内容:
• 属性必须符合KVO。KVO支持KVC的简单数据,包括OC对象、KVC支持的数值和结构体。
• 这个类能够发送这个属性的KVO变化通知。
• 依赖的键被适当地注册。
这里有两种技术能够确保变化通知能被发送。NSObject默认自动支持发送并且对所有符合KVC的属性都支持。通常,如果你符合标准的cocoa代码编写和命名规则,你可以使用这种自动变化通知,你不必编写任何额外的代码。
手动变化通知提供额外的控制通知什么时候发送,这需要编写额外的代码。你可以通过子类重写automaticallyNotifiesObserversForKey:类方法来控制哪些属性是自动变化通知。
自动变化通知
NSObject提供基本的自动键值变化通知。自动键值变化通知会通知观察者键值访问造成的变化,以及KVC方法造成的变化。自动通知也支持集合代理对象返回,例如mutableArrayValueForKey:。
// 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];
手动变化通知
在一些情况下,你可能控制通知的进程,例如,减少触发通知的次数可能对于某些应用来说不必要,或者组合一组变化为一个单独通知。手动变化通知提供一些方式来完成这些工作。
手动和自动通知并不是互斥的。你可以在自动通知的属性发送手动通知。你可能想要完全地控制特定属性的进程。在这种情况下,你需要重写NSObject的automaticallyNotifiesObserversForKey:方法。对于一些不使用自动变化通知的属性,你应该在automaticallyNotifiesObserversForKey:方法返回NO。子类应该调用父类的方法来处理不识别的键。
+ (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"];
}
在有序一对多关系中,你必须不仅指定这个键发生变化,还要指定变化类型和改变下标。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通知的多个依赖关系。
一对一关系
为了自动触发一对一关系通知,你应该重写keyPathsForValuesAffectingValueForKey:方法或者适当地实现注册依赖键模式的方法。
下面的例子提供一个依赖属性。
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
当firstName或者lastName发生变化时,监听fullName属性的观察者应该被通知。
一种方法是重写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;
}
通常你应该调用父类方法来获取包含任何成员的集合(set)并处理它(而不是完全重写父类的方法)。
你可以实现一些类方法来实现相同的结果,这些方法的命名符合keyPathsForValuesAffecting<Key>,这里的<Key>为依赖属性的名称(首字符为大写开头)。使用这种方式来重写keyPathsForValuesAffectingFullName:类方法。
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
当你在已有类的种类中添加计算性属性,你不能重写keyPathsForValuesAffectingValueForKey:方法,因为种类不支持重写方法。在这种情况下,实现keyPathsForValuesAffecting<Key> 类方法来实现这种机制。
一对多关系
keyPathsForValuesAffectingValueForKey:方法不支持一对多关系的键路径。
这里有2种方式来处理这种情况:
1. 你可以使用KVO来注册父对象(一对多的父对象)作为子对象(一对多的子对象)的相关属性的观察者,你必须为每一个子对象添加和删除父对象观察者。
- (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;
}
2. 如果你使用Core Data,你可以注册父对象为应用的通知中心观察者,这个父对象管理对象上下文。父对象应该响应子对象发送的变化通知。
KVO实现细节
自动键值观察的实现使用isa-swizzling技术。
isa指针就像暗示的那样,指向对象的类,这个类管理一个派发表。这个派发表本质上是包含类定义的方法和其他数据的指针,。
当观察者注册成为某个属性的观察者,被观察对象的isa指针会被修改,指向这个类的派生类而不是实际的类。结果是isa指针的值不在影响实际实例的类。
你不应该依赖isa指针来确定类的成员关系,而是使用class类方法来确定实例的类。