- 被观察的对象必须兼容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注册为观察者。