本文将主要介绍以下内容:
?
官方文档:
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
KVO是一种编程模式,当被观察的object的指定的属性发生变化时候,观察者object将会被告知。
KVO基于KVC,关于KVC可查阅Key-Value Coding Programming Guide。
先看官文的叙述:
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进行观察的。那什么样的类的属性才是可观察的呢?
当然,首先该类需要遵守KVC;那什么样的类才遵守KVC呢,我们要不要实现它的基础细节呢?
Objects typically adopt key-value coding when they inherit from NSObject (directly or indirectly), which both adopts the NSKeyValueCoding protocol and provides a default implementation for the essential methods.
以上Key-Value Coding 文档指出 :其实很简单,继承自NSObject的类就可以使用KVC,因为NSObject已经实现了NSKeyValueCoding 协议相应的基础功能;这样一来就满足KVO使用的要求了。
官文指出:注册观察者方法的调用,并不会强引用所涉及到的参数对象(被观察对象,观察者,context)。
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.
通常,为了监听变化,将实现该方法:
- (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]; }}
如上为了避免影响父类,会在分支语句的最后将调用父类方法。
此时如果所有父类(中间层类)都没有处理该变化,传到NSObject,后将会抛出一个异常(NSInternalInconsistencyException)。
If a notification propagates to the top of the class hierarchy, NSObject throws an NSInternalInconsistencyException because this is a programming error: a subclass failed to consume a notification for which it registered.
理论要求上,注册观察者的和移除观察者,两个方法应该是全局性成对的。
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 call removeObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception. 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.
如上,关于移除观察者的注意事项,官文给出了3个点:
- 向一个未注册成为观察者的 object 发送 removeObserver 消息,将引发异常(NSRangeException);
官文强调,addObserver 与 removeObserver必须对应起来调用;
如果不成对调用,建议将 removeObserver:forKeyPath:context: 放在 try/catch 中(PS:估计try/catch 用在removeObserver这么损的建议,基本不会有负责人会同意组员这么搞的)。- 观察者在已dealloc的情况下,也不会自动移除,此时被观察者的属性发生变化后,会继续发送通知给已经释放的观察者,这样就会触发异常。要求开发者需要确保,观察者在内存被回收之前,必须从被观察者那里移除。(PS:综合以上来看,观察者被引用的不是weak 型指针,应该是unsafe型的)。
- 开发者无法查询某个object是否是观察者或者被观察者,因为KVO的协议并没有提供任何相关的方法;官文建议开发者在object 初始化的时候注册观察则,并在dealloc前移除观察。(PS:如果是cell bind用途,且考虑到重用的话,这种建议无效)。
上面文档中介绍到,KVO协议没有任何方法获取观察者和被观察者属性,但是NSObject(NSKeyValueObservingCustomization)提供了一个看似有用的属性:
/* Take or return a pointer that identifies information about all of the observers that are registered with the receiver, the options that were used at registration-time, etc. The default implementation of these methods store observation info in a global dictionary keyed by the receivers' pointers. For improved performance, you can override these methods to store the opaque data pointer in an instance variable. Overrides of these methods must not attempt to send Objective-C messages to the passed-in observation info, including -retain and -release.*/@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
注释中提到,该属性引用了向object实例对象注册的所有observer,默认情况下observationInfo是存储在一个全局字典中,key使用的是观察者的指针(PS: 还记得上文提到的对观察者 unsafe 型的引用么,不难理解,想必就是这里了);为了性能起见,建议使用者重写,并提供自定义的数据类型(PS:非void *类型的,即可以明确知道内部布局的对象——举个栗子如容器中对应的泛型)。
Apple官文中这么介绍的:
Automatic key-value observing is implemented using a technique called isa-swizzling.The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
官文指出KVO的实现,使用了isa-swizzling。当一个object被观察后,该object的 isa 指针将会被修改指向新生成的中间类,而非之前的类;这样以来,isa 的值将不能真实的反映出该object的实际类型。开发者不应该依赖 isa 获取object 所属的 Class,而是应该调用 object的实例方法[object class] 获取object的具体类型。(PS:不了解 isa 的朋友自行查阅下,这里不多介绍了)
关于原理描述可参考下图理解:
虽然没有Apple的源码,但如果有同学真想要详细了解KVO的实现原理,可参考Github上的 Aspect 开源库,其源码会给你答案。
FBKVOController源码比较简单,感兴趣的可阅读 FBKVOController Github地址,以下主要讲下部分细节与大家可能漏掉的点。
类图如下,结构很简单:
类图中,下划线开头的类(_FBKVOInfo、_FBKVOSharedController)属于内部类,即并没有对外界public。
先来看看FBKVOController中是怎么描述的:
Key-value observing is a particularly useful technique for communicating between layers in a Model-View-Controller application. KVOController builds on Cocoa's time-tested key-value observing implementation. It offers a simple, modern API, that is also thread safe. Benefits include: Notification using blocks, custom actions, or NSKeyValueObserving callback. No exceptions on observer removal. Implicit observer removal on controller dealloc. Thread-safety with special guards against observer resurrection
提到四点:
想象一下当使用系统的KVO,我们需要严格的让addObser与removeObser 成对的调用(并且,大多数时候他们不在一个方法内,而是在两个不同的时机点);在调用removeObser的时候需要判断是否addObser某对象的某属性(如上KVO注意事项4-3中,官文明确指出了开发者无法查询某个object是否是观察者或者被观察者,这无疑增加了自己维护状态的难度),并且还需要判断是否已经移除过(移除两次将会报异常)。
线程安全。
_FBKVOSharedController中以下一段代码,主要针对当观察info中包含NSKeyValueObservingOptionInitial的时候做的特殊处理:
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info{ if (nil == info) { return; } // register info pthread_mutex_lock(&_mutex); [_infos addObject:info]; pthread_mutex_unlock(&_mutex); // add observer [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info]; if (info->_state == _FBKVOInfoStateInitial) { info->_state = _FBKVOInfoStateObserving; } else if (info->_state == _FBKVOInfoStateNotObserving) { // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions, // and the observer is unregistered within the callback block. // at this time the object has been registered as an observer (in Foundation KVO), // so we can safely unobserve it. [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; }}- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info{ if (nil == info) { return; } // unregister info pthread_mutex_lock(&_mutex); [_infos removeObject:info]; pthread_mutex_unlock(&_mutex); // remove observer if (info->_state == _FBKVOInfoStateObserving) { [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; } info->_state = _FBKVOInfoStateNotObserving;}
触发场景为——当info中包含NSKeyValueObservingOptionInitial,并且使用者在回调中写了 remove该观察对象时:
NSObject *object = xxx; NSObject *objectObservered = xxxx; __weak typeof(objectObservered) wkObservered = objectObservered; __weak typeof(object) wkObject = object; [objectObservered.KVOControllerNonRetaining observe:object keyPath:@"xxx" options:NSKeyValueObservingOptionInitial block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) { if (wkObservered && wkObject) { [wkObservered.KVOControllerNonRetaining unobserve:object keyPath:@"xxx"]; } }];
上面一段代码执行的时候,_FBKVOSharedController中调用顺序如下,并对代码分析:
-(void)unobserve:(id)object info:(nullable _FBKVOInfo *)info ,
此时info->_state = _FBKVOInfoStateInitial,
所以该方法中的条件语句并不会执行 if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
紧接着执行接下来一行代码 info->_state = _FBKVOInfoStateNotObserving; 此时state 被变更为_FBKVOInfoStateNotObserving。
if (info->_state == _FBKVOInfoStateInitial) { info->_state = _FBKVOInfoStateObserving; } else if (info->_state == _FBKVOInfoStateNotObserving) { // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions, // and the observer is unregistered within the callback block. // at this time the object has been registered as an observer (in Foundation KVO), // so we can safely unobserve it. [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; }
APPLE KVO官文 — Key-Value Observing Programming Guide
FBKVOController Github地址
Cocoa Bindings Programming Topics
Key-Value Coding Programming Guide
Aspect