键值观测(KVO)是一种不使用NSNotificationCenter的通知机制。一个对象可以通过KVO直接注册到第2个对象上,当第2个对象中的值发生变化时,第1个对象就会收到通知。此外,第2个对象(被观察的对象)不必做任何额外的事情,它甚至都意识不到注册已经发生了。当被观测对象中的值发生了变化,注册对象(观察者)就会自动收到通知(也许更好的一个架构上的类比就是目标-动作机制;这是一种介于任意两个对象之间的目标-动作机制)。
在使用KVO时,观察者就是你自己的对象;当观察者接收到被观察者改变的通知时,你需要编写代码进行响应。不过,被观察的对象(注册到其上以监听变化的对象)无需是你自己的对象;实际上,通常情况下它也不是你自己的对象。很多Cocoa对象的行为都是KVO形式的,你可以对其使用KVO。一般来说,KVO主要用于替代委托与通知。
KVO的使用可以划分为如下3个阶段:
注册
要想监听被观察对象中某个值的变化,你需要注册到被观察对象上。这通常需要调用被观察对象的addObserver:forKeyPath:options:context:方法(所有继承自NSObject的对象都有这个方法,因为它是通过非正式协议NSKeyValueObserving注入NSObject中的,而NSKeyValueObserving则是NSObject与其他类上的一组类别)。
变化
变化发生在被观察对象中的值上,而且方式比较特别,即必须以KVO兼容的形式。通常,这意味着要使用键值编码兼容的访问器来作出改变。通过键值编码兼容的访问器来设置属性。
通知
当被观察对象中的值发生变化时,观察者会自动收到通知:其observeValueForKeyPath:ofObject:change:context:方法(我们已经针对这个目的实现了该方法)会在运行期得到调用。
如果不想再接收通知了,那就需要取消对被观察对象的注册,这是通过向其发送removeObserver:forKeyPath:(或removeObserver:forKeyPath:context:)来实现的。这是非常重要的,原因与取消对NSNotification的注册相同:如果不取消注册,那么当通知发送给了已经不存在的观察者时,应用就会崩溃。你需要显式取消观察者所注册的每个键路径;不能将nil作为第2个参数来表示“所有键路径”。取消注册的最后一个机会是观察者的deinit;显然,这要求观察者拥有对被观察对象的引用。
事情还没有结束。在被观察对象销毁前,所有的观察者都必须要显式取消对其的注册!如果对象销毁了,但观察者并没有取消注册,那么应用就会崩溃,同时控制台会打印出一条消息:“An instance...was deallocated while key value observers were still registered with it.”
如下示例来自于我自己的代码。AVPlayerViewController是个视图控制器,其视图用于显示视频内容。当该视图首次出现时会闪一下,因为视图是黑色的,到视频内容出现前中间会有一点延时。解决办法就是一开始让视图不可见,直到视频内容出现后才让其可见。这样,我们希望当视频内容出现后能够收到通知。AVPlayerViewController有个readyForDisplay属性,我们希望该属性变为true时能够收到通知。不过,AVPlayerViewController并没有委托,也没有提供通知。那么,解决之道就是使用KVO:将自身注册到AVPlayerViewController上,监听其readyForDisplay属性的变化。如下代码展示了如何配置并呈现AVPlayerViewController的视图:
func setUpChild { // ... let av = AVPlayerViewController av.player = player av.view.frame = CGRectMake(10,10,300,200) av.view.hidden = true // looks nicer if we don/'t show until ready av.addObserver(self, forKeyPath: /"readyForDisplay/", options: , context: nil) ① // ...}override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<>) { ② if keyPath == /"readyForDisplay/" { if let obj = object as? AVPlayerViewController { dispatch_async(dispatch_get_main_queue, { self.finishConstructingInterface(obj) }) } }}func finishConstructingInterface (vc:AVPlayerViewController) { if !vc.readyForDisplay { return } vc.removeObserver(self, forKeyPath:/"readyForDisplay/") ③ vc.view.hidden = false}
①AVPlayerViewController的视图一开始是不可见的(hidden为true)。我们注册并监听其readyForDisplay属性的变化。
②AVPlayerViewController的readyForDisplay属性发生了变化,我们收到了通知,因为observeValueForKeyPath:...得到了调用。我们要确保这是个正确的通知;如果是,那么就继续完成界面的构建。注意到被观察对象(AVPlayerViewController)会作为object参数传递进来;这不仅有助于识别通知,还可以让我们与该对象通信。对于observeValueForKeyPath:...是在哪个线程上调用的是没有任何保证的,因此在做任何会影响界面的事情前我们需要移到主线程外。
③最后检查一次,确保readyForDisplay已经从false变为了true,取消注册(我们只需要监听其改变一次)并让视图可见(hidden为false)。
options:参数是个位掩码(NSKeyValueObservingOptions)。该参数可以将改变的属性的新值以change:字典的形式发给我们。这样,我们就可以改写代码,将检查readyForDisplay是否为true的代码移动到observeValueForKeyPath....实现中。现在的注册代码如下所示:
av.addObserver( self, forKeyPath: /"readyForDisplay/", options: .New, context: nil)
下面是剩余部分的代码;如第5章所述,这是一系列guad语句:
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<>) { guard keyPath == /"readyForDisplay/" else {return} guard let obj = object as? AVPlayerViewController else {return} guard let ok = change?[NSKeyValueChangeNewKey] as? Bool else {return} guard ok else {return} dispatch_async(dispatch_get_main_queue, { self.finishConstructingInterface(obj) })}func finishConstructingInterface (vc:AVPlayerViewController) { vc.removeObserver(self, forKeyPath:/"readyForDisplay/") vc.view.hidden = false}
你可能想知道addObserver:...与observeValueForKeyPath:....中context:参数的含义。基本上,我不建议你使用这个参数,不过无论怎样还是要介绍一下。context:参数表示传递给addObserver:...以及从observeValueForKeyPath:....获取的“任何数据”。不过,你需要注意其值,因为其类型是UnsafeMutablePointer<Void>。这意味着即便运行时持有它,其内存也不是由运行时管理的;你需要通过持有它的一个持久化引用来管理其内存。通常的做法是使用全局变量(声明在文件顶部的变量);为了防止任何地方都能访问这个变量,你可以将其声明为private的,如以下代码所示:
private var con = /"ObserveValue/"
在调用addObserver:...时,你会将该变量的地址&con作为context:参数传递进去。当observeValueForKeyPath:...接收到通知时,你可以将context:参数作为标识符,将其与&con进行比较:
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) { if context != &con { return // wrong notification } // ...}
在上述代码中,存储在全局变量中的值是没什么意义的;我们只是将其地址作为标识符而已。如果想要使用存储在全局变量中的值,请将UnsafeMutablePointer强制类型转换为另一个UnsafeMutablePointer底层类型。接下来就可以将底层值作为UnsafeMutablePointer的memory属性了。在该示例中,con是个String:
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) { let c = UnsafeMutablePointer<String>(context) let s = c.memory // /"ObserveValue/" // ... }
键值观测是个很复杂的机制;请查阅Apple的Key-Value Observing Guide了解详细信息(比如,可以观测可变的NSArray,不过其机制要比之前介绍的更加复杂)。KVO也有一些令人遗憾的缺点。首先,所有通知都是通过调用同一个方法出现的,而这个方法则会成为瓶颈,这非常遗憾。追踪谁观察了谁,确保观察者与被观察者都有恰当的生命周期并且能够及时取消注册是一件很棘手的事情。不过一般来说,KVO有助于确保不同对象中的值协调一致;如前所述,Cocoa中的一些地方希望你使用KVO。
KVO中被观察者与观察者都要继承自NSObject。此外,如果被观察的属性声明在Swift中,那就必须将其标记为dynamic,否则KVO将无法使用(原因在于KVO通过改写访问器方法来工作;Cocoa要能进入方法中并修改对象代码才可以,如果属性没有声明为dynamic,那么这一切是无法实现的)。