- 被观察的对象必须兼容KVO。一般情况下,如果对象是继承自
NSObject
的话,那么该对象及其属性都会自动兼容KVO。 - 观察者调用
addObserver:forKeyPath:options:context:
方法来将自身添加为观察者,观察对象的key path - 为了获取被观察对象的key path的变化,观察者还需要实现
observeValueForKeyPath:ofObject:change:context:
。 - 在结束观察(比方说对象被释放等情况)的话,观察者需要调用
removeObserver:forKeyPath:
来移除观察。 - KVO跟
NSNotificationCenter
不同的一点是,KVO没有一个集中的对象来提供通知给所有观察者。在被观察的对象发生变化时,KVO会直接给观察者发送消息,没有其他多余的操作。
addObserver:forKeyPath:options:context:
-
options:
NSKeyValueObservingOptionOld
: 获取改变之前的值NSKeyValueObservingOptionNew
: 获取改变之后的值NSKeyValueObservingOptionInitial
: 当某个属性进行了初始化时发送通知。只会接收到一次NSKeyValueObservingOptionPrior
: 在发生变回之前发送通知
-
context:
可以赋值为NULL,但是如果观察者的父类也同样观察者同一个key path时,会出现问题。
为了避免问题的发生,最好的方式是在类中创建一个静态变量来作为context.
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;复制代码
observeValueForKeyPath:ofObject:change:context:
所有观察者都必须实现此方法来接收通知!
removeObserver:forKeyPath:context:
- 如果你的对象没有被注册为观察者,而你又调用了移除观察者的方法的话,会出现
NSRangeException
错误。避免出现此错误的话,最好是注册和移除的方法配套使用,或者是将移除的方法放在try/catch中执行。 - 观察者对象在被摧毁时并不会自动移除自身的观察者身份。
- 通常在
init
或者viewDidLoad
中注册观察者身份,在dealloc
中移除。
自动发送消息
e.g. 能够引起KVO消息发送的例子
// 调用访问器方法[account setName:@"Savings"];// 使用 setValue:forKey:[account setValue:@"Savings" forKey:@"name"];// 使用 setValue:forKeyPath:[document setValue:@"Savings" forKeyPath:@"account.name"];复制代码
手动发送消息
某些情况下,你可能会想自己来管理消息的进程。比方说因为某些原因减少触发的消息数,又或者将几个通知合并为一个。要完成上面的需求的话,你就需要手动触发KVO的消息发送。
手动和自动发送消息并不会冲突。一般情况下,我们只会对某一个特殊的对象进行手动的消息处理。这样的话,我们在继承NSObject
的时候,需要复写automaticallyNotifiesObserversForKey:
方法,并且返回NO
.
e.g.
+ (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"];}// or- (void)setBalance:(double)theBalance { if (theBalance != _balance) { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; }}// 如果是一对多的关系的话,还需要修改对象改变的类型(NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement)以及所涉及到index- (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"];}复制代码
- To-One RelationShips
要自动触发一对一关系的通知的话,需要复写keyPathsForValuesAffectingValueForKey:
比方说有一个fullName
的属性
- (NSString *)fullName { return [NSString stringWithFormat:@"%@ %@", firstName, lastName];}复制代码
fullName的属性由firstName和lastName决定。在复写的时候,需要指定这两个相关的属性
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"fullName"]) { NSArray *affectingKeys = @[@"lastName", @"firstName"]; keyPaths = [keyPaths setByAppendingObjectsFromArray:affectingKeys]; } return keyPaths;}// 可以更改为+ (NSSet *)keyPathsForValuesAffectingFullName { return [NSSet setWithObjects:@"lastName", @"firstName", nil];}复制代码
- To-Many RelationShips
- 在被观察者的相关属性中注册观察者。
- (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的话,可以将managed context object注册为观察者。