KVO官方文档
You must perform the following steps to enable an object to receive key-value observing notifications for a KVO-compliant property:
- Register the observer with the observed object using the method addObserver:forKeyPath:options:context:.
- Implement observeValueForKeyPath:ofObject:change:context: inside the observer to accept change notification messages.
- Unregister the observer using the method removeObserver:forKeyPath: when it no longer should receive messages. At a minimum, invoke this method before the observer is released from memory.
你必须执行以下步骤来让一个对象能够接收一个符合KVO的属性的KVO通知:
- 通过被观察者对象调用方法 addObserver:forKeyPath:options:context:来注册观察者。
- 在观察者中实现方法observeValueForKeyPath:ofObject:change:context: 来接收变化通知消息。
- 当观察者不再接收消息时通过方法 removeObserver:forKeyPath:来移除观察者注册。至少在观察者从内存中释放之前调用这个方法。
Important: Not all classes are KVO-compliant for all properties. You can ensure your own classes are KVO-compliant by following the steps described in KVO Compliance. Typically properties in Apple-supplied frameworks are only KVO-compliant if they are documented as such.
重要:并非所有类的所有属性都符合KVO标准。 您可以按照 KVO Compliance中描述的步骤确保您自己的类符合KVO标准。 通常,Apple提供的框架中的属性仅在符合KVO标准的情况下才会被记录。
个人总结:KVO三步曲:注册观察者、实现回调、移除观察者
注册成为观察者
An observing object first registers itself with the observed object by sending an
addObserver:forKeyPath:options:context:
message, passing itself as the observer and the key path of the property to be observed. The observer additionally specifies an options parameter and a context pointer to manage aspects of the notifications.
观察者对象通过被观察者对象发送一条addObserver:forKeyPath:options:context:
消息来首次注册自己,将自己作为观察者和要被观察的属性的key path一起传递过去。观察者还指定了一个options参数和一个context指针来管理通知的各方面。
options
The options parameter, specified as a bitwise
OR
of option constants, affects both the content of the change dictionary supplied in the notification, and the manner in which notifications are generated.
options参数(指定为option常量的按位或)影响着通知中提供的关于变化的字典的内容以及通知生成的方式。
You opt to receive the value of the observed property from before the change by specifying option
NSKeyValueObservingOptionOld
. You request the new value of the property with optionNSKeyValueObservingOptionNew
. You receive both old and new values with the bitwiseOR
of these options.
你可以通过指定option为NSKeyValueObservingOptionOld
在变化发生之前来接收别观察的属性的值,指定option为NSKeyValueObservingOptionNew
来请求属性的新值。也可以使用这两个option的按位或来同时接收旧值和新值。
You instruct the observed object to send an immediate change notification (before
addObserver:forKeyPath:options:context:
returns) with the optionNSKeyValueObservingOptionInitial
. You can use this additional, one-time notification to establish the initial value of a property in the observer.
你可以使用optionNSKeyValueObservingOptionInitial
让被观察对象发送一条即时变化通知(在addObserver:forKeyPath:options:context:
返回之前)。你可以在观察者中使用这个额外的一次性的通知来建立属性的初始值。
You instruct the observed object to send a notification just prior to a property change (in addition to the usual notification just after the change) by including the option
NSKeyValueObservingOptionPrior
. The change dictionary represents a prechange notification by including the keyNSKeyValueChangeNotificationIsPriorKey
with the value of anNSNumber
wrappingYES
. That key is not otherwise present. You can use the prechange notification when the observer’s own KVO compliance requires it to invoke one of the -willChange…
methods for one of its properties that depends on an observed property. The usual post-change notification comes too late to invokewillChange…
in time.
你通过包含optionNSKeyValueObservingOptionPrior
来命令被观察对象只在出行变化之前发送一条通知(除了变化之后的通知)。变化字典通过包含键NSKeyValueChangeNotificationIsPriorKey
和将YES
封装成NSNumber
的值来表示一个预变化通知。这个key不代表其他意思。当观察者自己的KVO合规性要求它为其某个依赖于被观察属性的属性调用-willChange ...方法之一时,您可以使用预换通知。通常的更改后通知来得太晚,无法及时调用willChange。(。。。。。。)
context
The context pointer in the
addObserver:forKeyPath:options:context:
message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specifyNULL
and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.
在消息addObserver:forKeyPath:options:context:
中的context指针包含了相应的变化通知中将被传回给观察者的任意数据。你可以指定为NULL
并完全依靠key path字符串来确定变化通知的来源,但是如果一个对象的父类出于不同的原因也在观察相同的key path,这个方法可能会出现问题。
A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.
一个更安全,更具扩展性的方法时使用上下文来确定你接收的通知时为你的观察者预定的而不是父类。
The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. Listing 1 shows example contexts for the
balance
andinterestRate
properties chosen this way.
类中唯一命名的静态变量的地址确定了一个良好的上下文。在超类或者子类中以相同方式选择的上下文不太可能重叠。你可以为整个类选择单个上下文,并且依赖通知消息中的key path字符串来确定改变了什么。或者,你可以为每个被观察的key path创建一个不同的上下文,来完全绕过字符串比较的需要,从而实现更有效率的通知解析。Listing 1展示了使用这种方式选择的balance
和interestRate
属性的示例上下文。
Listing 1 Creating context pointers
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
Listing 2中展示了Person
示例使用给定的上下文将自身注册为Account
实例的balance
和interestRate
属性的观察者。
Listing 2 Registering the inspector as an observer of the balance and interestRate properties
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
Note: The key-value observing
addObserver:forKeyPath:options:context:
method does not maintain strong references to the observing object, the observed objects, or the context. You should ensure that you maintain strong references to the observing, and observed, objects, and the context as necessary.
注意:KVO方法addObserver:forKeyPath:options:context:
不保留对观察者对象,被观察者对象或上下文的强引用。你应该确保在必要时保留对观察者和被观察者对象以及上下文的强引用。
接收变化的通知
When the value of an observed property of an object changes, the observer receives an
observeValueForKeyPath:ofObject:change:context:
message. All observers must implement this method.
当一个对象的被观察属性的值发生改变时,观察者会收到一条observeValueForKeyPath:ofObject:change:context:
消息。所有的观察者都必须实现这个方法。
The observing object provides the key path that triggered the notification, itself as the relevant object, a dictionary containing details about the change, and the context pointer that was provided when the observer was registered for this key path.
观察对象提供key path来触发通知,它自己作为相应的对象,一个字典包含变化的详细信息,以及为key path注册观察者时提供上下文指针。
The change dictionary entry
NSKeyValueChangeKindKey
provides information about the type of change that occurred. If the value of the observed object has changed, theNSKeyValueChangeKindKey
entry returnsNSKeyValueChangeSetting
. Depending on the options specified when the observer was registered, theNSKeyValueChangeOldKey
andNSKeyValueChangeNewKey
entries in the change dictionary contain the values of the property before, and after, the change. If the property is an object, the value is provided directly. If the property is a scalar or a C structure, the value is wrapped in an NSValue object (as with key-value coding).
变化字典条目NSKeyValueChangeKindKey
提供发生改变的类型的信息。如果被观察对象的值发生变化了,NSKeyValueChangeKindKey
条目返回NSKeyValueChangeSetting
。依赖于观察者被注册时指定的选项,变化字典 里的NSKeyValueChangeOldKey
和 NSKeyValueChangeNewKey
条目包含了属性变化前后的值。如果属性是一个对象,值会直接被提供。如果属性时一个纯量或者C结构体,值会被封装成一个 NSValue对象(通过KVC)。
If the observed property is a to-many relationship, the
NSKeyValueChangeKindKey
entry also indicates whether objects in the relationship were inserted, removed, or replaced by returningNSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
, orNSKeyValueChangeReplacement
, respectively.
如果被观察的属性时一个一对多的关系,NSKeyValueChangeKindKey
条目也表示关系中的对象是否被插入,移除或者通过返回NSKeyValueChangeInsertion
, NSKeyValueChangeRemoval
, 或者 NSKeyValueChangeReplacement
来替换它。
The change dictionary entry for
NSKeyValueChangeIndexesKey
is anNSIndexSet
object specifying the indexes in the relationship that changed. IfNSKeyValueObservingOptionNew
orNSKeyValueObservingOptionOld
are specified as options when the observer is registered, theNSKeyValueChangeOldKey
andNSKeyValueChangeNewKey
entries in the change dictionary are arrays containing the values of the related objects before, and after, the change.
变化字典条目NSKeyValueChangeIndexesKey
是一个NSIndexSet
对象,用于指定变化关系中的索引。如果在注册观察者时将NSKeyValueObservingOptionNew或NSKeyValueObservingOptionOld指定为选项,则变化字典中的NSKeyValueChangeOldKey和NSKeyValueChangeNewKey条目是包含更改之前和之后的相关对象的值的数组。
The example in Listing 3 shows the
observeValueForKeyPath:ofObject:change:context:
implementation for thePerson
observer that logs the old and new values of the propertiesbalance
andinterestRate
, as registered in Listing 2.
Listing 3中的示例显示了observeValueForKeyPath:ofObject:change:context:
用于记录属性balance和interestRate的旧值和新值的Person观察者的实现,如Listing 2中所示
Listing 3 Implementation of observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
If you specified a NULL context when registering an observer, you compare the notification’s key path against the key paths you are observing to determine what has changed. If you used a single context for all observed key paths, you first test that against the notification’s context, and finding a match, use key path string comparisons to determine what specifically has changed. If you have provided a unique context for each key path, as demonstrated here, a series of simple pointer comparisons tells you simultaneously whether or not the notification is for this observer, and if so, what key path has changed.
当注册观察者时如果指定了一个NULL上下文,则将通知的key path和正在监听的key paths进行比较以确定改变的内容。如果您对所有观察到的key path使用了单个上下文,则首先针对通知的上下文对其进行测试,然后查找匹配项,使用key path字符串比较来确定具体更改的内容。 如果您为每个key path提供了唯一的上下文,如此处所示,一系列简单的指针比较会同时告诉您通知是否适用于此观察者,如果是,则更改了哪个key path。
In any case, the observer should always call the superclass’s implementation of
observeValueForKeyPath:ofObject:change:context:
when it does not recognize the context (or in the simple case, any of the key paths), because this means a superclass has registered for notifications as well.
在任何情况下,观察者在无法识别上下文时应该总是调用父类的实现(或者简单的情况下,任意的key path),因为这意味着父类也已经注册了通知。
Note: If a notification propagates to the top of the class hierarchy,
NSObject
throws anNSInternalInconsistencyException
because this is a programming error: a subclass failed to consume a notification for which it registered.
注意:如果一个通知传到类层次结构的顶部,NSObject
会抛出一个NSInternalInconsistencyException
异常,因为这是一个编程错误:一个子类无法去使用它注册的通知。
移除作为观察者的对象
You remove a key-value observer by sending the observed object a
removeObserver:forKeyPath:context:
message, specifying the observing object, the key path, and the context. The example in Listing 4 showsPerson
removing itself as an observer ofbalance
andinterestRate
.
通过向被观察者对象发送一个removeObserver:forKeyPath:context:
消息并指定观察对象,key path和上下文来移除键值观察。Listing 4的示例展示了Person
移除自身作为balance
和 interestRate
的观察者。
Listing 4 Removing the inspector as an observer of balance and interestRate
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
After receiving a
removeObserver:forKeyPath:context:
message, the observing object will no longer receive anyobserveValueForKeyPath:ofObject:change:context:
messages for the specified key path and object.
当收到一条removeObserver:forKeyPath:context:
消息后,观察对象将不再接收任何针对指定key path和对象的observeValueForKeyPath:ofObject:change:context:
消息。
When removing an observer, keep several points in mind:
当移除一个观察者时,要记住以下几点:
- Asking to be removed as an observer if not already registered as one results in an
NSRangeException
. You either callremoveObserver:forKeyPath:context:
exactly once for the corresponding call toaddObserver:forKeyPath:options:context:
, or if that is not feasible in your app, place theremoveObserver:forKeyPath:context:
call inside a try/catch block to process the potential exception.
如果还没有注册观察者就被要求作为观察者被移除会导致NSRangeException
异常。对相应的 addObserver:forKeyPath:options:context:
调用只可以调用一次removeObserver:forKeyPath:context:
,或者如果在你的APP中不可行的话,把removeObserver:forKeyPath:context:
调用放在try/catch代码块中去处理潜在的异常。
个人总结:必须要先注册观察者才能调用移除观察者方法,如果还未注册就调用移除方法会出现异常。
- An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
观察者在销毁时不会自动移除自己。被观察的对象继续发送通知,无视观察者的状态。然而,变化通知像其他消息一样发送给已释放的对象,会触发内存访问异常。因此在观察者从内存中消失之前要确定移除自身。
个人总结:观察者对象在释放之前一定要手动移除观察,否则可能会导致内存泄漏。
The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.
协议没有提供什么方式去查询一个对象是否是观察者或者被观察者。组织代码来避免发生相关的错误。一个典型的模式是在观察者初始化时注册成为观察者(例如在init或viewDidLoad中),在销毁时取消注册(通常在dealloc中),确保正确的成对和有顺序的添加和移除消息,并且在观察者从内存中释放之前取消注册。
个人总结:观察者的注册和移除必须成对存在,并且要先注册后移除,需要开发者自身进行检查。