关于kvo即Key-Value Observing ,下面记下读官方文档https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html的个人总结,一方面希望能够加深对kvo的了解的,另一方面能够和大家一起讨论,如有错误,请留言指正,万分感谢。
一、什么是kvo
kvo是一个对象用来观察另一个对象(可以是自己)属性的变化,并作出处理的一种机制。当被观察对象的属性变化时,系统会发出一个通知,来通知观察对象。kvo实现机制的前提是对象遵守kvc非正式协议,所有继承于NSObject的对象,并且按一般命名规则定义属性的,都遵守kvc机制。
假设有一个Person对象,他有一个Account属性。他需要当账户发生变动时接收到一个通知,首先account要注册person为其观察者addObserver:forKeyPath: options:context:,person对象为了收到通知,需实现observeValueForKeyPath:ofObject:change:context:方法,如果不想监听账户的变化了,那么需要在person 对象dealloc调用之前,调用removeObserver:forKeyPath:
移除对账户特定属性的监听.
Options
addObserver:forKeyPath: options:context:,参数options 传入按位或运算的常量,不但影响到通知的内容,还影响到通知产生的方式,通过N
SKeyValueObservingOptionOld
.来获取属性变化之前的值,通过NSKeyValueObservingOptionNew获取属性变化之后的值,通过或运算 | 来同时获取变化之前和变化之后的值即传入NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew。也可传入NSKeyValueObservingOptionInitial来让被观察对象在addObserver:forKeyPath: options:context:return之前发出一个一次通知。 可以用来在observeValueForKeyPath:ofObject:change:context:初始化观察者对象属性的值。options的值传入NSKeyValueObservingOptionPrior可以在被观察对象属性的值发生变化之前,由被观察对象发出一个通知.
Context 可以包含任意的数据,并被传参到OberserveValueForKeyPath:ofObject:change:context:方法中,你可以传入NULL,但是当观察对象的父类对象同时对该KeyPath注册了观察者的时候就会产生问题,因此安全的做法是可以使用context来区分观察者是子类还是父类。你可以为所有的类定义一个context,而用keyPath来区分观察的属性,也可以为所有要观察的对象的属性来定义不同的context,然后通过不同的context来区分观察的属性。记住在找不到对应的属性时一定要调用【super observeValueForKeyPath:ofObject:change:context:]方法,因为有可能是父类的监听
举例:
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext; | ||||||||||||||||||||||||||||||||
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
|
当你不需要监听被观察对象属性变化的时候一定要记得调用 removeObserver: forKeyPath: context方法,并且在调用这个方法之前,一定要确认 已经注册了观察者,否者会抛出NSRangeException异常,当然可以在try-catch block里来调用捕获异常。观察对象并不会自动移除观察模式,即使它已经被dealloc掉了,因此它会继续接受被观察者对象发出的通知,而对一个被销毁的对象接受通知会导致内存异常。协议并没有提供是否是观察者或者是被观察者的方法,因此需要合理的组织代码,一般在初始化或者viewDidLoad里注册观察者,在dealloc里移除观察者。
- (void)unregisterAsObserverForAccount:(Account*)account { |
[account removeObserver:self |
forKeyPath:@"balance" |
context:PersonAccountBalanceContext]; |
|
[account removeObserver:self |
forKeyPath:@"interestRate" |
context:PersonAccountInterestRateContext]; |
}
|
二、如何手动控制kvo的过程
那么什么样的类的属性是遵守kvo的呢?1、首先这个类的属性必须是遵守kvc的2这个类为这个属性的变化发送kvo通知3、所依赖的key要被正确注册
什么情况下改变属性会发送kvo通知呢?1.使用通用的set 方法,2.使用kvc中的setValueForKey:和setValueForKeyPath:都会使类自动发送kvo通知。
手动发送kvo通知:在有些情况下,你想控制发送通知的过程,来减少不必要的通知,或者是将一组数据的变化通知,改成一个通知。想控制通知的发送过程需要重写automaticallyNotifiesObserversForKey:类方法。对于手动管理的通知的属性返回NO, 对于不手动控制通知过程的属性,用super调用该方法。举例如下:
+ (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 { |
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"]; |
} |
- (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"]; |
} |
三、关于有依赖关系的Keys kvo的处理
在许多情况下一个对象的属性值,依赖于其他对象的多个属性的值,因此当依赖对象的一个属性值发生变化时,需要收到通知。例如一个人的名字包含姓和名,获取全名的getter方法如下
- (NSString *)fullName { |
return [NSString stringWithFormat:@"%@ %@",firstName, lastName]; |
} |
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { |
|
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; |
|
if ([key isEqualToString:@"fullName"]) { |
NSArray *affectingKeys = @[@"lastName", @"firstName"]; |
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; |
} |
return keyPaths; |
} |
+ (NSSet *)keyPathsForValuesAffectingFullName { |
return [NSSet setWithObjects:@"lastName", @"firstName", nil]; |
} |
- (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; |
} |