KVO(键值观察)是Objective-C提供的一种观察对象属性变化的机制,其内部是利用KVC技术来实现观察者设计模型。利用KVO用户能够注册一个对象为另外一个对象的观察者,并在被观察对象的属性发生变化时能收到通知。 css
1 使用KVO 框架
利用键值观察(Key Value Observing),能够自动观察其余对象的变化。所以,当一个对象改变状态(属性)时,其它对象就会获得通知。经过键值观察,你不须要手动告诉其余对象进行更新。它们会自动收到新值并执行适当的操做。这极其强大。设置是该技术最强大的应用之一,此外, Cocoa 框架中的核心数据和其余技术也利用了键值观察实现了一些奇妙的功能。 函数
要使用KVO须要进行一些操做和配置: url
1) 要使用键值观察,被观察的对象必须对所观察的特性(属性)使用符合 KVC 标准的存取器方法。 spa
2) 想要观察变化的对象,也就是观察者,必须实现一个接收通知的回调方法。该方法是-observeValue:forKeyPath:ofObject:change:context:。该方法在值变化时被调用并能够配置成同时接收新值和原值以及其余自定义的信息。 设计
3) 观察者经过调用-addObserver:forKeyPath:options:context:方法针对被观察对象进行注册。调用该方法,告诉对象要观察的 KVC 键路径以及但愿看到的变化,并提供一个在收到变化通知时能够传回的上下文对象。 code
注意观察者完成这些配置后,键路径指定的属性的任何变化均可以自动调用观察者的回调方法。在观察者完成对被观察对象的观察后,必须将本身移除。若是没有作到这点而且观察者以后就释放了,未来向该观察者发送通知时可能会致使应用崩溃。 server
2 注册成为观察者 对象
注册成为观察者很容易。针对想要观察的对象简单调用-addObserver:forKeyPath:options:context:方法, blog
Observer 参数一般是 self,这是在被观察值变化时收到通知的对象。键路径参数指定想要观察的特性的键路径。 options 参数指定一些标记来告诉 KVO 你但愿变化如何传给你。这些值能够经过|操做符进行或操做。传入的可能值如表 21所示。
表 21 传入的可能值
值 |
功 能 |
NSKeyValueObservingOptionNew |
做为变动信息的一部分发送新值 |
NSKeyValueObservingOptionOld |
做为变动信息的一部分发送旧值 |
NSKeyValueObservingOptionInitial |
在观察者注册时发送一个初始更新 |
NSKeyValueObservingOptionPrior |
在变动先后分别发送变动,而不仅在变动后发送一次 |
上下文参数是一个在 KVO系统中无变动传递的void*参数,即当被观察的属性发生变化时,会被传回给观测对象的回调方法。本质上,就 KVO 而言,该参数是不透明的数据块,任何今后传入参数的都会无变动传递的。
注意:
记住使用 void*上下文参数时有和垃圾回收相关的特殊规则,你必须确保 void*指向的数据在以后访问时仍然没有被释放并有效。换句话说,不要将一些存储在栈上的值传递给该参数,这会致使崩溃。
3 实现KVO的回调方法
使用 KVO 的第二步就是编写观察者的回调方法。以下的实现代码显示了-observeValue:forKeyPath:ofObject:change:context:方法的一个示例实现。
能够从该方法看出,要作的第一件事情就是找出被观察对象中变化的特性。该方法自动传入一个对象参数,告诉你哪一个对象向你发送通知。经过对键路径的传入值使用-isEquals 方法,你能够准确地肯定对象的什么特性发生了改变。 Key 参数仅仅是一个字符串,和对 KVC 使用时同样。所以,可使用 NSString 方法-isEqualToString:来肯定该通知所对应的键路径。
在肯定了对象的哪一个特性发生变化后,你能够执行任何合适的操做。实际的变化经过 change参数传递给你。该参数是一个 NSDictionary 对象,包括和你注册成为观察者时所请求的变化信息相关的键和值。这些键和值如表 22所示。
表 22 和变化信息相关的键值
键 |
值 |
NSKeyValueChangeKindKey |
指定变化类型的 NSNumber |
NSKeyValueChangeNewKey |
新值 |
NSKeyValueChangeOldKey |
原值 |
NSKeyValueChangeIndexeskey |
若是 NSKeyValueChangeKindKey 是 NSKeyValueChangeInsertion、 NSKeyValueChangeRemoval、NSKeyValueChangeReplacement 之一,该值就包含变化值的索引 |
NSKeyValueChangeNotificationIsPriorKey |
和 NSKeyValueChangeOptionPrior 结合使用来表示这是"以前"的通知 |
能够看出,若是选择同时接收原值和新值,两个都会在变化参数中提供,经过合适的键就能够访问到。从变化字典中获取到值以后,就能够在对象中使用它执行任何须要的操做。
记住 KVC 必须使用对象来发送值——不能直接使用标量和结构体。所以,若是所观察的值是标量或者结构体,所接收的值就分别是 NSNumber 或 NSValue 类型的。所以,必须从该值中提取出实际须要的标量或者结构体值。上述示例代码就展现了这一点。
NSKeyValueChangeKindKey 指定了接收到的变化类型。可能的类型如表 23所示。
表 23 可能的变化类型
值 |
功 能 |
NSKeyValueChangeSetting |
指定该值被设置 |
NSKeyValueChangeInsertion |
指定这些值插入到集合或者一对多的关系中 |
NSKeyValueChangeRemoval |
指定这些值在一对多的关系中被移除 |
NSKeyValueChangeReplacement |
指定这些值在一对多的关系中被替换 |
4 移除观测者
记住在结束对一个对象变化的观察后,须要移除观察者。若是不这样,应用可能会崩溃。
为了移除观察者,只须要调用方法-removeObserver:forKeyPath:,并传入观察者做为第一个参数,观察的键路径是第二个参数。代码清单 6-16 显示了一个在观察者的 dealloc 方法中实现的示例。
说明:
在垃圾回收的环境中,若是忘记移除观察者可能不会形成崩溃。可是,移除观察者还是一种好的作法,这样就能够在不支持垃圾回收的环境中造成该习惯。
5 实现手动通知
全部的这些通知都自动发生。须要作的就是为属性提供符合 KVC 标准的存取器方法,其余一切都会正常工做。有时,不必定想利用自动通知。有时想在改变一个值或者一组值时手动发送通知。好比,若是须要一次性进行不少变动,可能会想将这些变化打包后一块儿发送。在这些状况下就会使用手动通知。
为了实现手动通知,必须重写类方法+automaticallyNotifiesObserversForKey:来告诉 Objective-C 你不想自动通知观察者所发生的变化。能够经过对任意一个想进行手动通知的键返回 NO 来实现。示例如代码清单 6-17 所示。
若是想要手动通知所发生的变化,你必须在变化以前调用方法-willChangeValueForKey:,而后在变化以后调用方法-didChangeValueForKey:。示例如代码清单 6-18 所示。
这些调用在须要时是能够嵌套的,好比在一次调用中须要修改多个变量的状况。此外还有和一对多关系对应的调用。它们是-willChange:valuesAtIndexes:forKey:和-didChange:valuesForIndexes:forKey:。
6 使用KVO的风险
使用 KVO 也会遇到问题,更具体点说,使用 KVO 最大的风险就是若是观察者观察每一步,这些观察者可能会执行其余操做,由于你没法控制这些观察者,因此也就没法控制这些操做。
大多数状况下这不会成为一个问题,但也在例外。这种状况就是在初始化函数或者 dealloc函数中使用存取器方法来释放变量成员时,如代码清单 6-19 所示。
按这种方式写 dealloc 方法很好!能够在释放成员变量的同时将它设成nil,一步搞定。问题就是,在调用这些存取器方法时, KVO 观察者会在这些变化发生时收到通知。若是他们不想接收 nil 或者但愿在收到通知时可以处理对象自己,此时就会发生一些糟糕的事情。此外,若是想到了观察者在收到 bar 变量的变化通知时,但愿能够访问 foo 变量,这种状况下就会形成一个问题,由于 foo 变量已经被释放而且被设置成 nil。
苹果目前推荐的作法就是不要在初始化函数或者 dealloc 方法中经过存取器方法初始化和释放成员变量。这种状况在 64 位运行时中变得更复杂,由于它能够在没有相关的成员变量的状况下声明属性。在这些状况中,初始化和释放成员变量的惟一办法就是经过存取器方法。
我是使用存取器方法来初始化和释放成员变量的,除非我知道在给定的环境中这样作会形成问题。此外,实现键值观察者时,我会确保观测者能够正确处理 nil 值并尽可能最小化反作用。
若是你以为这种风险是值得的,那就经过存取器方法来编写初始化函数和释放函数吧。只要意识到可能的危险,在遇到问题时,就能够立刻知道应该从哪里查找。另外一方面,若是你不能确保观察者会这么作的话,那就就遵循苹果的建议,除非不得已,不然不要在初始化函数和析构函数中使用存取器方法。