KVO是对Objective-c观察者模式的实现,也是OC非常强大和有用的特性,同时还是实现cocoa bndings的基础(-bind:toObject:withKeyPath:options:
)。因此,理解和掌握KVO可以给我们带来很多便利以及意想不到的效果。
KVO可以让观察者在被观察者的属性被修改时直接接收到通知。在KVO的作用下,一个对象可以观察另一个对象的任何属性,同时,也可以知道某个属性的修改前和修改后的值。对多关系的观察者不仅能知道改变发生的类型,而且还能知道哪些对象被改变了。
在通知机制中,KVO与NSNotification
提供的通知机制类似,但是也存在很鲜明的区别。NSNotification
是以广播的形式将通知传递给所有注册为观察者的对象,即“一对多”,而KVO则会在属性值发生改变时直接将通知传递给观察者,即“点对点”。
由于基类NSObject
提供了对于KVO的基本实现,因而所有的cocoa对象本质上都能够使用KVO。为了能够接收到KVO的通知,你必须要做以下几件事:
你必须确保被观察类的属性符合KVO规则(KVO Compliance)。KVO规定被观察对象的类必须符合KVC规则(KVC Compliance)同时允许属性的自动观察者通知或者手动实现属性的KVO。
给值会发生改变的对象(即被观察者)添加观察者。你可以通过调用
addObserver:forKeyPath:options:context:
方法做到。在观察者对象中,实现方法
observeValueForKeyPath:ofObject:change:context:
。该方法在被观察对象的属性值发生变化时会被调用。
第二点和第三点很好理解,第一点则需要多说一点。并不是说所有类的属性都满足KVO规则,他需要满足以下几个条件:
类的属性必须满足KVC规则。KVO与KVC支持的数据类型相同,包括OC对象、标量和结构体。
类要发送属性变化的KVO通知。
依赖的键需要被正确的注册。
关于第一点,何为满足KVC规则?对于一个满足KVC规则的指定属性,它必须实现valueForKey:
和setValue:forKey:
来作用于指定属性。更详细地:
对于対一关系的属性,它的类必须做到:
- 实现方法名为
-<key>
,-is<key>
的方法,或者创建对象名为或者_ 的实例对象。 - 如果属性是可变的,那么它还需要实现
-set<key>:
方法。-set<Key>:
的方法实现不应该包含验证操作。- 若key需要进行验证它的类还需要实现
-validate<Key>:error:
方法。对于有索引的对多关系的属性,它的类必须要做到:
- 实现方法名为
-<key>
的方法并返回数组。- 或者有变量名为
或_ 的实例变量。 - 或者实现方法
-countOf<key>
和-objectIn<key>``-<key>AtIndexes:
两个方法中的一个。- 另外,你可以实现
-get<key>:range:
来优化性能。对于无序的对多关系的属性,它的类需要做到:
- 实现方法名为
-<key>
的方法并并返回一个集合。- 或者创建变量名为
或_ 的实例变量。 - 或者实现方法
-countOf<key>
、-enumeratorOf<key>
和-memberOf<key>:
。
若该属性还是可变的,它的类还需做到:- 实现
-add<key>Object:
和-add<key>:
中的一个或两个。- 实现
-remove<key>Object:
和-remove<key>:
中的一个或两个。- 另外,你可以实现
-intersect<key>:
和-set<key>:
来优化性能。
关于第二点,如何确保通知被发送?对于所有满足KVC规则的类的属性已经自动支持KVO。因此,只要你遵守标准Cocoa编码和命名规范,你可以使用自动变化通知(Automatic Change Notification)————你不需要编写额外的代码。
以下代码示例会触发KVO变化通知被发送:
|
|
在某些情况下,你想控制通知的进程,比如想减少不必要的原因而触发通知的次数,或者想把多个变化整合进一个通知当中。这时候自动变化通知就无法满足需求,而手动变化通知(Manual Change Notification)则派上用场了。
在这种情况下,你需要重写NSObject
的automaticallyNotifiesObserversForKey:
方法。若你想将某个属性从自动变化通知中移除,则在automaticallyNotifiesObserversForKey:
方法中返回NO。
|
|
为了实现手动变化通知,你需要在改变变量值�前触发willChangeValueForKey:
方法并在改变变量值后触发didChangeValueForKey:
方法。
你可以通过检验值是否发生改变来减少不必要的通知发送。
若一个操作引起多个属性值发生变化,你必须像下面这样嵌套变化的通知:
在有序的对多关系的属性中,你必须不仅指明改变的属性,而且还需指明改变的类型和涉及到的对象的索引值。改变的类型是NSKeyValueChange
枚举,包含NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement。受影响的对象的索引值传入的是NSIndexSet对象。
关于第三点,在很多情况下某个属性依赖于另一个对象的一个或多个属性。如果一个属性值发生改变,那么受影响的属性其值也会发生改变。那么如何确保存在依赖关系的属性的KVO通知能正确发送?
在対一关系中,为了触发自动通知你需要重写keyPathsForValuesAffectingValueForKey:
方法。
比如,一个人的名字包含姓和名。他的名字的全称可以用以下方法来写:
当firstName或者lastName属性值发生改变时,观察fullName的对象必须接收到通知。其中一个解决方法是重写keyPathsForValuesAffectingValueForKey:
方法来指明fullName属性依赖于lastName和firstName属性。
如上面代码示例中,你的重写方法实现中必须要触发super方法调用,并且返回的集合中要包含父类的keys。
你还可以通过实现命名规则遵循keyPathsForValuesAffecting<Key>
(Key为属性名并且首字母大写)的类方法来得到同样的结果。使用该方案对上面代码进行改写如下:
当你使用分类给现有的类添加属性时,不能重写keyPathsForValuesAffectingValueForKey:
方法,因为在分类中不支持重写该方法。在这种情况下,只能通过实现keyPathsForValuesAffecting<Key>
类方法来达到效果。
在对多关系中,keyPathsForValuesAffectingValueForKey:
方法不支持。比如,你有一个Deprtment对象与employee对象的对多关系,并且employee有salary属性。在Department对象中有一个totalSalary属性依赖于所有的employee的salary的关系。
这里有两种解决方案:
1.你可以使用KVO注册父类(Department)为所有子类(employee)的salary属性的观察者。你必须在添加和移除子类对象的过程中添加和移除观察者。以下为示例代码:
2.如果你使用Core Data,你可以注册父类为它管理对象上下文的观察者。当子类相关属性值发生改变其父类就会得到相应的通知,类似于KVO。