KVO(键值观察)提供了一种机制,允许将其他对象的特定属性的更改通知给对象。
你可以观察包括简单属性、对一关系和对多关系在内的属性。对多关系的观察者被告知所做更改的类型,以及更改中涉及哪些对象。
KVO的主要好处是,你不必实现自己的方案来在每次属性更改时发送通知。其定义良好的基础设施具有框架级支持,这使其易于采用—通常不需要向项目添加任何代码。此外,该基础设施已经具有完整的功能,这使得对单个属性和依赖值支持多个观察器变得很容易。
注册KVO(Registering for Key-Value Observing)
你必须执行以下步骤使对象能够接收符合kvo的属性的KVO通知:
- 使用
addObserver:forKeyPath:options:context:
方法将观察者注册到被观察对象。 - 在观察者内部实现
observeValueForKeyPath:ofObject:change:context:
以接受更改通知消息。 - 当观察器不再应该接收消息时,使用
removeObserver:forKeyPath:
方法注销它。至少,在观察者从内存中释放之前调用此方法。
注册为观察者
观察对象首先通过由被观察对象发送addObserver:forKeyPath:options:context:
消息注册,将自己作为观察者,将要观察的属性的keyPath
传递给被观察对象。观察者还指定了一个options
参数和一个context
指针来管理通知的各个方面。
Options
参数
options
参数指定为选项常量的位或,它会影响通知中提供的更改字典(changes
)的内容,以及生成通知的方式。
通过指定
NSKeyValueObservingOptionOld
选项,你可以选择从更改之前接收观察到的属性的值。你可以使用选项NSKeyValueObservingOptionNew
请求属性的新值。通过这些选项的位或(NSKeyValueObservingOptionOld |NSKeyValueObservingOptionNew
),你将同时收到旧值和新值。使用选项
NSKeyValueObservingOptionInitial
让被观察对象发送一个立即更改通知(在addObserver:forKeyPath:options:context: returns之前),你可以使用这个附加的一次性通知在观察者中获得属性的初始值。通过包含选项
NSKeyValueObservingOptionPrior
,你可以指示被观察对象在属性更改之前发送一个通知(除了在更改之后发送通常的通知)。更改字典(changes
)通过包含键NSKeyValueChangeNotificationIsPriorKey
和包装YES
的NSNumber值来表示一个预更改通知。这个key
没有出现在其他地方。当观察器自身的KVO遵从性要求它为依赖于被观察属性的某个属性调用-willChange…
方法时,可以使用预更改通知。通常的变更后通知来得太晚,无法及时调用willChange…
。
Context
参数
addObserver:forKeyPath:options:context:
消息中的context
指针包含将在相应的更改通知中传递回观察者的任意数据。你可以指定NULL并完全依赖keyPath
字符串来确定更改通知的起源,但是这种方法可能会导致问题——观察者对象的父类(继承链中的任意一个)出于不同的原因监听了相同的keyPath
。
一种更安全、更可扩展的方法是使用context
来确保接收到的通知将发送给你的观察者,而不是发送给父类。
类中唯一命名的静态变量的地址是一个很好的context
。在父类或子类中以类似方式选择的上下文不太可能重叠。你可以为整个类选择一个context
,并依赖于通知消息中的键路径字符串来确定更改的内容。或者,你可以为每个观察到的键路径创建不同的context
,这完全绕过了字符串比较的需要,从而实现更高效的通知解析。
1 | static void *PersonAccountBalanceContext = &PersonAccountBalanceContext; |
注意:KVO的addObserver:forKeyPath:options:context:
方法不维护对观察对象、被观察对象或上下文的强引用。你应该确保在必要时保持对观察对象、被观察对象、对象和上下文的强引用。
接收属性变更通知(Receiving Notification of a Change)
当一个对象的被观察属性的值发生变化时,观察者会收到一个observeValueForKeyPath:ofObject:change:context:
消息。所有的观察者都必须实现这个方法。
观察对象提供触发通知的keyPath
(本身作为相关对象)、包含关于更改的详细信息的NSDictionary
以及在为该keyPath
注册观察者时提供的上下文指针。
更改字典条目NSKeyValueChangeKindKey
提供了关于发生的更改类型的信息。如果被观察对象的值发生了变化,NSKeyValueChangeKindKey
表项将返回NSKeyValueChangeSetting
。根据注册观察者时指定的选项,更改字典中的NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
key包含更改之前和更改之后的属性值。如果属性是一个对象,则直接提供值。如果属性是标量
或C结构体
,则值被包装在NSValue
对象中(与KVC一样)。
如果观察到的属性是一对多关系,变更字典海通过keyNSKeyValueChangeKindKey
分别返回NSKeyValueChangeInsertion
、NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
来指示关系中的插入、删除或替换。
同时,变更字典中的keyNSKeyValueChangeIndexesKey
对应的是一个NSIndexSet
对象,指定关系中发生更改的索引。如果NSKeyValueObservingOptionNew
或NSKeyValueObservingOptionOld
在观察者注册时被指定为选项,则更改字典中的keyNSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
是包含更改之前和更改之后相关对象的值的数组。
在任何情况下,当观察者不识别上下文(或者在简单的情况下,任何keyPath
)时,应该总是调用父类的实现observeValueForKeyPath:ofObject:change:context:
,因为这意味着父类类也注册了通知。
注意:如果一个通知传播到类层次结构的顶部,NSObject抛出一个NSInternalInconsistencyException
,这是一个编程错误:子类未能使用它注册的通知。
移除观察者对象(Removing an Object as an Observer)
通过向被观察对象发送removeObserver:forKeyPath:context:
消息,指定观察对象
、keyPath
和context
,可以删除键值观察器。
在接收到removeObserver:forKeyPath:context:
消息后,观察对象将不再接收指定keyPath
和对象
的observeValueForKeyPath:ofObject:change:context:
通知。
移除观察者时,请记住以下几点:
- 如果观察者还没有被注册,请求被移除会导致
NSRangeException
。你要么调用removeObserver:forKeyPath:context:
恰好一次对应的addObserver:forKeyPath:options:context:
,或者把removeObserver:forKeyPath:context:
调用放在一个try/catch
块中来处理潜在的异常。 - 观察者在被释放时不会自动移除自身。被观察对象继续发送通知,而不关心观察者的状态。然而,与任何其他消息一样,发送到已释放对象的更改通知将触发内存访问异常。因此,你要确保观察者在从内存中消失之前删除自己。
- KVO没有提供询问对象是观察者还是被观察者的方法。注意构建代码以避免出现相关的错误。一个典型的模式是在观察者的初始化(例如在
init
或viewDidLoad
中)期间注册为观察者,在释放(通常在dealloc中)期间取消注册,确保正确配对和有序的添加和删除消息,并在从内存中释放观察者之前取消注册。
注册依赖key(Registering Dependent Keys)
在许多情况下,一个属性的值依赖于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生了更改,那么派生属性的值也应该被标记为更改。如何确保为这些依赖属性发布键值观察通知取决于关系的基数。
一对一关系(To-One Relationships)
要为一对一关系自动触发通知,你应该要么重写keyPathsForValuesAffectingValueForKey:
,要么实现一个合适的方法,遵循它定义的注册依赖键的模式。
例如,一个人的全名取决于他的姓和名。返回全名的方法可以写成如下形式:
1 | - (NSString *)fullName { |
当firstName
或lastName
属性发生变化时,必须通知观察fullName
属性的观察者,因为它们会影响该属性的值。
一个解决方案是覆盖keyPathsForValuesAffectingValueForKey:
指定一个人的fullName属性依赖于lastName和firstName属性。
1 | + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { |
你的重写通常应该调用父类并返回一个集合,该集合中包含由此操作产生的任何成员(以便不干扰超类中此方法的重写)。
你还可以通过实现一个遵循命名约定keypathsforvaluesinfluence<Key>
的类方法来实现相同的结果,其中<Key>
是依赖于值的属性名(首字母大写)。
1 | + (NSSet *)keyPathsForValuesAffectingFullName { |
当你使用category
将计算类属性添加到现有类时,你不能覆盖keyPathsForValuesAffectingValueForKey:
方法,因为你不能在类别中重写方法。 在这种情况下,实现一个匹配的keyPathsForValuesAffecting<Key>
类方法来利用这个机制。
一对多关系(To-Many Relationships)
keyPathsForValuesAffectingValueForKey:
方法不支持包含一多对关系的keyPath。例如,假设你有一个Department
对象,它与Employee
有一对多的关系(employees
),而Employee
有一个salary
属性。你可能希望Department
对象具有一个totalSalary
属性,该属性依赖于关系中所有employee
的salary
。你不能这样做:在keyPathsForValuesAffectingTotalSalary
方法中返回employees.salary
作为keyPath
。
在这种情况下,你可以使用KVO
来将父节点(在本例中为Department)注册为所有子节点(在本例中为Employees)相关属性的观察者。当向关系中添加和从关系中删除子对象时,必须将父对象作为观察者添加和删除。在observeValueForKeyPath:ofObject:change:context:
方法中,更新相关值以响应更改,如下面的代码片段所示:
1 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { |