NSKeyValueObserving
非正式协议定义了一种机制,它允许对象去监听其它对象的某个属性的修改。
我们可以监听一个对象的属性,包括简单属性,一对一的关系,和一对多的关系。一对多关系的监听者会被告知集合变更的类型,以及哪些对象参与了变化。
NSObject
提供了一个NSKeyValueObserving
协议的默认实现,它为所有对象提供了一种自动发送修改通知的能力。我们可以通过禁用自动发送通知并使用这个协议提供的方法来手动实现通知的发送,以便更精确地去处理通知。
在这里,我们将通过具体的实例来看看NSKeyValueObserving
提供了哪些方法。我们的基础代码如代码清单1所示:
代码清单1:示例基础代码
|
|
在这段代码中,我们定义一两个类,一个是PersonObject
类,这个类的对象在下面将充当观察者的角色。另一个是BankObject
类,我们在这个类中定义了三个属性,作为被监听的属性。由于NSObject
类已经实现了NSKeyValueObserving
协议,所以我们不需要再显式地去让我们的类实现这个协议。
接下来,我们便来看看NSKeyValueObserving
协议有哪些功能。
注册/移除观察者
要让一个对象监听另一个对象的属性的变化,首先需要将这个对象注册为相关属性的观察者,我们可以使用以下方法来实现:
|
|
这个方法带有四个参数,描述如下:
anObserver
:观察者对象,这个对象必须实现observeValueForKeyPath:ofObject:change:context:
方法,以响应属性的修改通知。keyPath
:被监听的属性。这个值不能为nil。options
:监听选项,这个值可以是NSKeyValueObservingOptions
选项的组合。关于监听选项,我们会在下面介绍。context
:任意的额外数据,我们可以将这些数据作为上下文数据,它会传递给观察者对象的observeValueForKeyPath:ofObject:change:context:
方法。这个参数的意义在于用于区分同一对象监听同一属性(从属于同一对象)的多个不同的监听。我们将在下面看到。
监听选项是由枚举NSKeyValueObservingOptions
定义的,是传入-addObserver:forKeyPath:options:context:
方法中以确定哪些值将被传到-observeValueForKeyPath:ofObject:change:context:
方法中。这个枚举的定义如下:
|
|
需要注意的是,当设定了NSKeyValueObservingOptionPrior
选项时,第一条通知不会包含NSKeyValueChangeNewKey
。当观察者自身的KVO需要为自己的某个属性调用-willChange...
方法,而这个属性的值又依赖于被观察对象的属性时,我们可以使用这个选项。
另外,在添加观察者时还有两点需要注意的是:
- 调用这个方法时,两个对象(即观察者对象及属性所属的对象)都不会被
retain
。 - 可以多次调用这个方法,将同一个对象注册为同一属性的观察者(所有参数可以完全相同)。这时,即便在所有参数一致的情况下,新注册的观察者并不会替换原来观察者,而是会并存。这样,当属性被修改时,两次监听都会响应。
对于第2点,我们在代码清单2中来验证一下:
代码清单2:验证多次使用相同参数来添加观察者的实际效果
|
|
(注,以上代码为在MRC环境下调用,确保personInstance
和bankInstance
不会被释放。)
这段代码的输出如下所示:
|
|
可以看到KVO为每次注册都调用了一次监听处理操作。所以多次调用同样的注册操作会产生多个观察者。另外,多个观察者之间的observeValueForKeyPath:ofObject:change:context:
方法调用顺序是按照先进后出的顺序来的(所有的监听信息都是放在一个数组中的,我们将在下面了解到)。
一个良好的实践是在观察者不再需要监听属性变化时,必须调用removeObserver:forKeyPath:
或removeObserver:forKeyPath:context:
方法来移除观察者,这两个方法的声明如下:
|
|
这两个方法会根据传入的参数(主要是keyPath
和context
)来移除观察者。如果observer
没有监听keyPath
属性,则调用这两个方法会抛出异常。大家可以试一下,程序会果断的崩溃。并报类似于以下的错误:
|
|
所以,我们必须确保先注册了观察者,才能调用移除方法。
那如果我们忘记调用移除观察者方法,会怎么样呢?我们来制造一个场景,看看会是什么结果。还是使用上面的代码,只不过这次我们在ARC下来测试:
代码清单3:未移除观察者的影响
|
|
其输出结果如下所示:
|
|
程序在调用一次KVO后,很爽快地崩溃了。给我们的解释是bankInstance
被释放了,但KVO中仍然还有关于它的注册信息。实际上,我们上面说过,在添加观察者的时候,观察者对象与被观察属性所属的对象都不会被retain
,然而在这些对象被释放后,相关的监听信息却还存在,KVO做的处理是直接让程序崩溃。
处理属性修改通知
当被监听的属性修改时,KVO会发出一个通知以告知所有监听这个属性的观察者对象。而观察者对象必须实现
-observeValueForKeyPath:ofObject:change:context:
方法,来对属性修改通知做相应的处理。这个方法的声明如下:
|
|
这个方法有四个参数,描述如下:
keyPath
:即被观察的属性,与参数object
相关。object
:keyPath
所属的对象。change
:这是一个字典,它包含了属性被修改的一些信息。这个字典中包含的值会根据我们在添加观察者时设置的options
参数的不同而有所不同。context
:这个值即是添加观察者时提供的上下文信息。
在我们的示例中,这个方法的实现是打印一些基本的信息。如代码清单1所示。
对于第三个参数,我们通常称之为变化字典(Change Dictionary),它记录了被监听属性的变化情况。我们可以通过以下key来获取我们想要的信息:
|
|
其中,NSKeyValueChangeKindKey
的值取自于NSKeyValueChange
,它的值是由以下枚举定义的:
|
|
通知观察者属性的变化
通知观察者的方式有自动与手动两种方式。
默认情况下是自动发送通知,在这种模式下,当我们修改属性的值时,KVO会自动调用以下两个方法:
|
|
这两个方法的任务是告诉接收者给定的属性将要或已经被修改。需要注意的是不应该在子类中去重写这两个方法。
但如果我们希望自己控制通知发送的一些细节,则可以启用手动控制模式。手动控制通知提供了对KVO更精确控制,它可以控制通知如何以及何时被发送给观察者。采用这种方式可以减少不必要的通知,或者可以将多个修改组合到一个修改中。
实现手动通知的类必须重写NSObject
中对automaticallyNotifiesObserversForKey:
方法的实现。这个方法是在NSKeyValueObserving
协议中声明的,其声明如下:
|
|
这个方法返回一个布尔值(默认是返回YES),以标识参数key指定的属性是否支持自动KVO。如果我们希望手动去发送通知,则针对指定的属性返回NO。
假设我们希望PersonObject
对象去监听BankObject
对象的bankCodeEn
属性,并希望执行手动通知,则可以如下处理:
代码清单4:关闭属性的自动通知发送
|
|
这样我们便可以手动去发送属性修改通知了。需要注意的是,对于对象中其它没有处理的属性,我们需要调用[super automaticallyNotifiesObserversForKey:key]
,以避免无意中修改了父类的属性的处理方式。
现在我们已经通过+automaticallyNotifiesObserversForKey:
方法设置了对象中哪些属性需要手动处理。接下来就是实际操作了。为了实现手动发送通知,我们需要在修改属性值前调用willChangeValueForKey:
,然后在修改属性值之后调用didChangeValueForKey:
方法。继续上面的示例,我们需要对bankCodeEn
属性做如下处理:
代码清单5:手动控制通知发送
|
|
如果我们希望只有当bankCodeEn
实际被修改时发送通知,以尽量减少不必要的通知,则可以如下实现:
代码清单6:在发送通知前测试值是否修改
|
|
我们来测试一下上面这段代码的实际效果:
代码清单7:测试避免属性未实际修改下不发送通知
|
|
这段代码的输出结果如下所示:
|
|
我们可以看到只输出了一次,而不是两次。
如果我们在setter
方法之外改变了实例变量(如_bankCodeEn
),且希望这种修改被观察者监听到,则需要像在setter
方法里面做一样的处理。这也涉及到我们通常会遇到的一个问题,在类的内部,对于一个属性值,何时用属性(self.bankCodeEn
)访问而何时用实例变量(_bankCodeEn
)访问。一般的建议是,在获取属性值时,可以用实例变量,在设置属性值时,尽量用setter
方法,以保证属性的KVO
特性。当然,性能也是一个考量,在设置值时,使用实例变量比使用属性设置值的性能高不少。
另外,对于一对多关系的属性,如果想手动处理通知,则可以使用以下几个方法:
|
|
同样,在子类中也不应该去重写这几个方法。
计算属性(注册依赖键)
有时候,我们的监听的某个属性可能会依赖于其它多个属性的变化(类似于swift
,可以称之为计算属性),不管所依赖的哪个属性发生了变化,都会导致计算属性的变化。对于这种一对一(To-one
)的关系,我们需要做两步操作,首先是确定计算属性与所依赖属性的关系。如我们在BankObject
类中定义一个accountForBank
属性,其get
方法定义如下:
代码清单8:计算属性
|
|
定义了这种依赖关系后,我们就需要以某种方式告诉KVO
,当我们的被依赖属性修改时,会发送accountForBank
属性被修改的通知。此时,我们需要重写NSKeyValueObserving
协议的keyPathsForValuesAffectingValueForKey:
方法,该方法声明如下:
|
|
这个方法返回的是一个集合对象,包含了那些影响key指定的属性依赖的属性所对应的字符串。所以对于accountForBank
属性,该方法的实现如下:
代码清单9:accountForBank属性的keyPathsForValuesAffectingValueForKey方法的实现
|
|
我们来再来看看监听accountForBank
属性是什么效果:
代码清单10:监听accountForBank属性
|
|
其输出结果为:
|
|
可以看到,不管是accountBalance
还是bankCodeEn
被修改了,都会发送accountForBank
属性被修改的通知。
需要注意的就是当我们重写+keyPathsForValuesAffectingValueForKey:
时,需要去调用super
的对应方法,并返回一个包含父类中可能会对key
指定属性产生影响的属性集合。
另外,我们还可以实现一个命名为keyPathsForValuesAffecting\<Key\>
的类方法来达到同样的目的,其中<Key>
是我们计算属性的名称。所以对于accountForBank
属性,还可以如下实现:
|
|
两种方法的实现效果是一样的。不过更建议使用后面一种方法,这种方法让依赖关系更加清晰明了。
集合属性的监听
keyPathsForValuesAffectingValueForKey:
只支持一对一的关系,而不支持一对多的关系,即不支持对集合的处理。
对于集合的KVO
,我们需要了解的一点是,KVO
旨在观察关系(relationship
)而不是集合。对于不可变集合属性,我们更多的是把它当成一个整体来监听,而无法去监听集合中的某个元素的变化;对于可变集合属性,实际上也是当成一个整体,去监听它整体的变化,如添加、删除和替换元素。
在KVC中,我们可以使用集合代理对象(collection proxy object)来处理集合相关的操作。我们以数组为例,在我们的BankObject
类中有一个departments
数组属性,如果我们希望通过集合代理对象来负责响应departments
所有的方法,则需要实现以下方法:
|
|
因此,我们的实现以下几个方法:
代码清单11:集合代码对象的实现
|
|
实现以上方法之后,对于不可变数组,当我们调用[bankInstance valueForKey:@"departments"]
的时候,便会返回一个由以上方法来代理所有调用方法的~对象。这个代理数组对象支持所有正常的NSArray
调用。换句话说,调用者并不知道返回的是一个真正的NSArray
,还是一个代理的数组。
另外,对于可变数组的代理对象,我们需要实现以下几个方法:
|
|
这些方法分别对应插入、删除和替换,有批量操作的,也有只改变一个对象的方法。可以根据实际需要来实现。
另外,对于可变集合,我们通常不使用valueForKey:
来获取代理对象,而是使用以下方法:
|
|
通过这个方法,我们便可以将可变数组与强大的KVO
结合在一起。KVO
机制能在集合改变的时候把详细的变化放进change
字典中。
我们先来看看下面的代码:
代码清单12:使用真正的数组对象监听可变数组属性
|
|
其输出为:
|
|
可以看到通过这种方法,我们获取的是真正的数组,只在departments
属性整体被修改时,才会触发KVO
,而在添加元素时,并没有触发KVO
。
现在我们通过代理集合对象来看看:
代码清单13:使用代理集合对象监听可变数组属性
|
|
其输出是:
|
|
可以看到,在往数组中添加对象时,也触发了KVO,并将改变的详细信息也写进了change
字典。在第二个消息中,kind
的值为2,即表示这是一次插入操作。同样,可变数组的删除,替换操作也是一样的。
集合(Set
)也有一套对应的方法来实现集合代理对象,包括无序集合与有序集合;而字典则没有,对于字典属性的监听,还是只能作为一个整理来处理。
如果我们想到手动控制集合属性消息的发送,则可以使用上面提到的几个方法,即:
|
|
不过得先保证把自动通知关闭,否则每次改变KVO
都会被发送两次。
监听信息
如果我们想获取一个对象上有哪些观察者正在监听其属性的修改,则可以查看对象的observationInfo
属性,其声明如下:
|
|
可以看到它是一个void
指针,指向一个包含所有观察者的一个标识信息对象,这些信息包含了每个监听的观察者,注册时设定的选项等等。我们还是用示例来看看。
代码清单14:observationInfo的使用
|
|
其输出结果如下:
|
|
我们可以看到observationInfo
指针实际上是指向一个NSKeyValueObservationInfo
对象,它包含了指定对象上的所有的监听信息。而每条监听信息而是封装在一个NSKeyValueObservance
对象中,从上面可以看到,这个对象中包含消息的观察者、被监听的属性、添加观察者时所设置的一些选项、上下文信息等。
NSKeyValueObservationInfo
类及NSKeyValueObservance
类都是私有类,我们无法在官方文档中找到这两个类的实现。不过从一些对系统库dump
出来的头文件,我们可以对这两个类有一些初步的了解。这里有一个对iOS SKD 4.3
的Foundation.framework
的dump
头文件,可以找到这两个类的头文件,其中NSKeyValueObservationInfo
的头文件信息如下所示:
|
|
可以看到其中有一个数组来存储NSKeyValueObservance
对象。
NSKeyValueObservance
类的头文件信息如下:
|
|
可以看到其中包含了一个监听的基本要素。在此不再做深入分析(没有源代码,深入不下去了啊)。
我们再回到observationInfo
属性本身来。在文档中,对这个属性的描述有这样一段话:
|
|
即这个方法的默认实现是以对象的指针作为key,从一个全局的字典中获取信息。由此,我们可以理解为,KVO的信息是存储在一个全局字典中,而不是存储在对象本身。这类似于Notification
,所有关于通知的信息都是放在NSNotificationCenter
中。
不过,为了提高效率,我们可以重写observationInfo
属性的set和get方法,以将这个不透明的数据指针存储到一个实例变量中。但是,在重写时,我们不应该尝试去向这些数据发送一个Objective-C
消息,包括retain
和release
。
KVO的实现机制
【本来这一小节是想放在另一篇总结中来写的,但后来觉得还是放在这里比较合适,所以就此添加上】
了解了NSKeyValueObserving
所提供的功能后,我们再来看看KVO的实现机制,以便更深入地的理解KVO。
KVO据我所查还没有开源(若哪位大大有查到源代码,还烦请告知),所以我们无法从源代码的层面来分析它的实现。不过Mike Ash的博文(译文见参考文献4)为我们解开了一些谜团。
基本的思路是:Objective-C
依托于强大的runtime
机制来实现KVO。当我们第一次观察某个对象的属性时,runtime
会创建一个新的继承自这个对象的class
的subclass
。在这个新的subclass
中,它会重写所有被观察的key
的setter
,然后将object
的isa
指针指向新创建的class
(这个指针告诉Objective-C
运行时某个object
到底是什么类型的)。所以object
神奇地变成了新的子类的实例。
嗯,让我们通过代码来看看实际的实现:
代码清单15:探究KVO的实现机制
|
|
这段代码的输出如下:
|
|
从输出中可以看到,bankInstance2
监听accountBalance
属性后,其实际上所属的类已经不是BankObject
了,而是继承自BankObject
的NSKVONotifying_BankObject
类。同时,NSKVONotifying_BankObject
类重写了setAccountBalance
方法,这个方法将实现如何通知观察者们的操作。当改变accountBalance
属性时,就会调用被重写的setAccountBalance
方法,并通过这个方法来发送通知。
另外我们也可以看到bankInstance2
对象的打印[bankInstance2 class]
时,返回的仍然是BankObject
。这是苹果故意而为之,他们不希望这个机制暴露在外面。所以除了重写相应的setter
,所以动态生成的NSKVONotifying_BankObject
类还重写了class
方法,让它返回原先的类。
小结
KVO作为Objective-C
中两个对象间通信机制中的一种,提供了一种非常强大的机制。在经典的MVC架构中,控制器需要确保视图与模型的同步,当model
对象改变时,视图应该随之改变以反映模型的变化;当用户和控制器交互的时候,模型也应该做出相应的改变。而KVO便为我们提供了这样一种同步机制:我们让控制器去监听一个model
对象属性的改变,并根据这种改变来更新我们的视图。所有,有效地使用KVO,对我们应用的开发意义重大。
别话:对KVO的总结感觉还是意犹未尽,总感觉缺少点什么,特别是在对集合这一块的处理。还请大家多多提供指点。