1.KVO简介
键值观察是一种机制,它允许对象在其他对象的指定属性发生更改时得到通知。为了理解键值观察,必须首先理解键值编码。
键值观察提供了一种机制,允许对象在其他对象的特定属性发生更改时得到通知。它对于应用程序中的模型层和控制器层之间的通信特别有用。控制器对象通常观察模型对象的属性,视图对象通过控制器观察模型对象的属性。此外,模型对象可以观察其他模型对象(通常用于确定依赖值何时发生变化),甚至可以观察自身(同样用于确定依赖值何时发生变化)。
也可以观察属性,包括简单属性、对一关系和对多关系。对多关系的观察者被告知所做更改的类型,以及更改中涉及的对象。
一个简单的例子说明了KVO如何在您的应用程序中发挥作用。假设一个Person对象与一个Account对象交互,该对象表示该个人在银行的储蓄帐户。Person的一个实例可能需要知道帐户实例的某些方面何时发生变化,比如余额或利率。
如果这些属性是Account的公共属性,用户可以通过定期轮询帐户来发现更改,但这是低效的,通常不切实际。更好的方法是使用KVO,当账户信息有更改时,会通知用户发生的更改。
要使用KVO,首先必须确保观察到的对象(本例中的帐户)是符合KVO的。通常,如果对象继承自NSObject,并且以通常的方式创建属性,那么将自动符合KVO,当然也可以手动实现遵从性。
接下来,必须将观察者实例Person注册到观察到的实例Account中。Person向帐户发送一个addObserver:forKeyPath:options:context:消息,对于每个观察到的密钥路径只发送一次,并将自己命名为观察者。
为了接收来自帐户的更改通知,所有观察者都需要(Person)实现observeValueForKeyPath:ofObject:change:context:方法。每当注册的密钥路径发生更改时,该帐户将向该人员发送此消息。然后,该人员可以根据更改通知采取适当的操作。
最后,当它不再需要观察时,并且至少在释放通知之前,Person实例必须通过向帐户发送消息removeObserver:forKeyPath:来注销。
上面是KVO实现的一个逻辑流程.
2.KVO的使用
1.使用addObserver:forKeyPath:options:context:方法将观察者注册到观察对象中。
- 1.options参数被指定为按位或选项常量,影响通知中提供的更改字典的内容和生成通知的方式.
NSKeyValueObservingOptionNew //更改字典应包含被监听属性的新值 NSKeyValueObservingOptionOld //更改字典应包含被监听属性的旧值 NSKeyValueObservingOptionInitial //当指定了这个选项时: 在addObserver:forKeyPath:options:context:消息被发出去后,甚至不用等待这个消息返回,观察者会马上收到一个通知。 观察者只会收到一次该通知,这可以用来确定被观察属性的初始值。 当同时指定New | Old | OptionInitial这3个选项时,观察者接收到的change字典中只会包含新值,而不会包含旧值。但是这个值对于观察者来说是新的。 NSKeyValueObservingOptionPrior //当指定了这个选项时: 在被观察的属性改变前,观察者就会收到一个通知(一般的通知发出时机都是在属性改变后,虽然change字典中包含了新值和旧值,但是通知还是在属性改变后才发出), change会包含一个NSKeyValueChangeNotificationIsPriorKey的键,值为一个NSNumber类型的YES。同时指定OptionPrior | New | Old时,change字典会包含旧值而不会包含新值。 你可以在这个通知中调用- (void)willChangeValueForKey:(NSString *)key;
- 2.context 上下文参数
1.可以设值为NULL,但是这样会存在一些问题,该对象的父类也会因为不同的原因观察相同的键路径. 举个例子:A类同时观察了B类和C类中相同名称属性的值的改变,那么使用相同的keyPath就没有那么方便与安全了! 2.更安全,可扩展的使用方法是使用类中唯一命名的静态变量的地址作为上下文,可以为整个类选择一个上下文, 并依赖于通知消息中的键路径字符串来确定更改了什么 也可以为每个观察到的键路径创建不同的上下文,从而完全绕过字符串比较的需要,从而实现更有效的通知解析。
- 3.注意
观察键值的addObserver:forKeyPath:options:context:方法不维护对观察对象、观察对象或上下文的强引用。 确保在必要时维护对观察对象、被观察对象和上下文的强引用。
- 4.示例:
//上下文 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]; }
2.实现observeValueForKeyPath:ofObject:change:context:在观察者内部接受更改通知消息。
- 1.当观察的对象属性值发生变化时,观察者会收到observeValueForKeyPath:ofObject:change:context:消息。所有观察者都必须实现这个方法。
- 2.keyPath:观察对象提供触发通知的键路径; object:本身作为相关的对象,change:一个包含有关更改的详细信息的字典,context:当观察者为这个键路径注册时提供的上下文指针。
- 3.change字典五个常量键的含义:
NSString *const NSKeyValueChangeKindKey; //提供了有关所发生更改的类型的信息。 //如果观察到的对象的值改变了,NSKeyValueChangeKindKey返回值NSKeyValueChangeSetting。 //根据观察者注册时指定的选项,change字典中的NSKeyValueChangeOldKey和NSKeyValueChangeNewKey包含了变更之前和之后属性的值。 //如果属性是对象,则直接提供值。如果属性是标量或C结构,则值被包装在NSValue对象中(与键值编码一样)。 //如果观察到的属性是一对多关系,NSKeyValueChangeKindKey还记录了被观察的对象是被插入(NSKeyValueChangeInsertion)、删除(NSKeyValueChangeRemoval),还是被替换(NSKeyValueChangeReplacement)。 NSString *const NSKeyValueChangeNewKey; // 被观察属性改变后新值的key,当观察的属性为一个集合对象, //且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时, //该值返回的是一个数组,包含插入,替换后的新值(删除操作不会返回新值) NSString *const NSKeyValueChangeOldKey; //被观察属性改变前旧值的key,当观察的属性为一个集合对象, //且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时, //该值返回的是一个数组,包含删除,替换前的旧值(插入操作不会返回旧值) NSString *const NSKeyValueChangeIndexesKey; //如果NSKeyValueChangeKindKey的值为NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement, //这个键的值是一个NSIndexSet对象,包含了增加,移除或者替换对象的index。 NSString *const NSKeyValueChangeNotificationIsPriorKey; //如果注册观察者时options指明了NSKeyValueObservingOptionPrior, //change字典中就会带有这个key,值为NSNumber类型的YES.
- 4.示例:Person观察者的observeValueForKeyPath:ofObject:change:context:的实现,该观察者记录属性balance和interestRate的新旧值。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context==PersonAccountBalanceContext) { //余额改变的监听 } else if (context == PersonAccountInterestRateContext) { //利率改变的监听 } else { //其他无法识别的context [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
- 5.如果在注册观察者时指定上下文为NULL,那么就需要将通知的键路径与正在观察的键路径进行比较,来确定发生了什么变化。如果对所有观察到的键路径使用单一上下文,那么首先要根据通知的上下文对其进行测试,并找到匹配项,然后使用键路径字符串比较来确定具体更改了哪些内容。如果是为每个键路径提供了惟一的上下文,就像示例一样,只需要比较上下文指针来确定更改的建路径的值。
- 6.在任何情况下,如果context(键路径)是一个无法识别的上下文(键路径),观察者应该调用observeValueForKeyPath:ofObject:change:context的父类实现,因为这意味着一个父类也注册了该通知。
3.使用removeObserver:forKeyPath方法注销观察者:当它不再应该接收消息时。至少,在观察者从内存释放之前调用这个方法。
- 1.如果注册了观察者,那么就需要在观察者内存释放之前移除它!
- 2.通过向观察对象发送removeObserver:forKeyPath:context:消息,指定观察对象、键路径和上下文,可以删除键值观察者。
- 3.在接收到removeObserver:forKeyPath:context:消息后,观察对象将不再接收到任何指定键值路径和上下文的observeValueForKeyPath:ofObject:change:context:消息。
- 4.移除观察者时,要记住以下几点:
1.当你向一个不是观察者的对象发送remove消息的时候(或者你重复发送remove消息时,再或者在注册观察者前就调用了remove), 则会抛出一个NSRangeException异常,所以,保险的做法是,把remove操作放在try/catch中。 2.一个监听者在其被销毁时,并不会自己注销监听,而给一个已经销毁的监听者发送通知,会造成野指针错误。所以至少保证,在监听者被释放前,将其监听注销。保证有一个add方法,就有一个remove方法 3.观察者在释放时不会自动移除自身。被观察的对象发送通知的时候,不会理会观察者的状态。 所以与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。 因此,要确保观察者在从内存中消失之前删除自己。 4.该协议没有提供询问对象是否是观察者或被观察者的方法,所以我们添加观察者与移除观察者需要成对出现。 典型的模式是: 在观察者的初始化过程中注册观察者(例如在init或viewDidLoad中), 在deallocation过程中取消注册(通常在dealloc中), 确保正确地匹配和有序地添加和删除消息,并且在观察者从内存中释放之前取消注册。 - (void)unregisterAsObserverForAccount:(Account*)account { [account removeObserver:self forKeyPath:@"balance" context:PersonAccountBalanceContext]; [account removeObserver:self forKeyPath:@"interestRate" context:PersonAccountInterestRateContext]; }
触发KVO的两种方式:
1.自动触发
理解了KVC,本质上是要调用setter,KVO是基于KVC实现的,所以我们想要触发KVO,就需要调用被观察对象的setter:
//以下四种方法均可触发KVO发送通知
[account setName:@"Savings"];
[account setValue:@"Savings" forKey:@"name"];
[document setValue:@"Savings" forKeyPath:@"account.name"];
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
//mutableArrayValueForKey:也是KVC的方法!如果用KVO监听了一个集合对象(比如一个数组),
//如果使用addObject:发送消息,是不会触发KVO通知的。
//但是通过mutableArrayValueForKey:这个方法对集合对象进行的相关操作(增加,删除,替换元素)就会触发KVO通知。(具体为什么,后期文章会说到)
2.手动触发
重写NSObject类方法:+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey,根据theKey来判断对应键值路径的属性是否开启自动通知。这样我们可以灵活的区分哪些被观察属性需要监听,哪些不需要监听。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要实现手动的触发通知,我们还需要实现:
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
这样就基本实现了一个KVO的手动通知,当该属性值改变时,观察者就能收到改变通知了。