Apple官文中的KVO 与 FBKVOController

时间:2021-07-28 18:04:02

前言


本文将主要介绍以下内容:

  • 详细列出Apple官文中KVO的注意事项(Apple KVO相关的引用皆摘自Apple官文)。
  • 介绍FBKVOController,以及它如何避免系统提供的KVO坑点。

Apple官文中的KVO


关于KVO

?

官方文档:

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


KVO使用注意事项


1.注册观察者


先看官文的叙述:

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使用的要求了。


2.对象的引用状态

官文指出:注册观察者方法的调用,并不会强引用所涉及到的参数对象(被观察对象,观察者,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.


3.观察者监听方法可能带来的异常


通常,为了监听变化,将实现该方法:

- (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.


4.移除观察者的注意事项


理论要求上,注册观察者的和移除观察者,两个方法应该是全局性成对的。

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个点:

  1. 向一个未注册成为观察者的 object 发送 removeObserver 消息,将引发异常(NSRangeException);
    官文强调,addObserver 与 removeObserver必须对应起来调用;
    如果不成对调用,建议将 removeObserver:forKeyPath:context: 放在 try/catch 中(PS:估计try/catch 用在removeObserver这么损的建议,基本不会有负责人会同意组员这么搞的)。
  2. 观察者在已dealloc的情况下,也不会自动移除,此时被观察者的属性发生变化后,会继续发送通知给已经释放的观察者,这样就会触发异常。要求开发者需要确保,观察者在内存被回收之前,必须从被观察者那里移除。(PS:综合以上来看,观察者被引用的不是weak 型指针,应该是unsafe型的)。
  3. 开发者无法查询某个object是否是观察者或者被观察者,因为KVO的协议并没有提供任何相关的方法;官文建议开发者在object 初始化的时候注册观察则,并在dealloc前移除观察。(PS:如果是cell bind用途,且考虑到重用的话,这种建议无效)。


5.系统中提供的观察者容器信息


上面文档中介绍到,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 KVO的实现


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 与 FBKVOController



KVO的详细原理


虽然没有Apple的源码,但如果有同学真想要详细了解KVO的实现原理,可参考Github上的 Aspect 开源库,其源码会给你答案。


FBKVOController




FBKVOController源码比较简单,感兴趣的可阅读 FBKVOController Github地址,以下主要讲下部分细节与大家可能漏掉的点。

FBKVOController类图:


类图如下,结构很简单:


Apple官文中的KVO 与 FBKVOController

类图中,下划线开头的类(_FBKVOInfo、_FBKVOSharedController)属于内部类,即并没有对外界public。


FBKVOController的优点:


先来看看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

提到四点:

  1. 关于回调,观察者可以使用Blocks,自定义的SEL,或者使用系统的回调方法(options参数是NSKeyValueObservingOptions)。
  2. 即使不手动移除observer也不会报异常。
  3. 当controller (object.KVOController/object.KVOControllerNonRetaining) dealloc时,将会隐式的移除当前object观察的所有对象。
  4. 线程安全,可以异步操作添加、移除观察者。


使用FBKVOController的原因


  1. 想象一下当使用系统的KVO,我们需要严格的让addObser与removeObser 成对的调用(并且,大多数时候他们不在一个方法内,而是在两个不同的时机点);在调用removeObser的时候需要判断是否addObser某对象的某属性(如上KVO注意事项4-3中,官文明确指出了开发者无法查询某个object是否是观察者或者被观察者,这无疑增加了自己维护状态的难度),并且还需要判断是否已经移除过(移除两次将会报异常)。

  2. 关于回调,FBKVOController提供了三种选择性,其中Block与SEL的回调形势能让代码更简洁明了,更符合“提炼函数”的重构组织形势。
  3. 线程安全。


FBKVOController的部分实现


对相关参数的引用状态


  1. 对observer弱引用;
  2. 对于被观察者的引用可选强引用、弱引用(object.KVOController、object.KVOControllerNonRetaining)。建议使用弱引用,原因很简单,我们不保持对被观察者的强引用,且系统KVO对于所有参数也是非强引用。当然如果选择强引用也是可以的,只不过,这意味着,在观察者生命周期内将强制维持被观察者无法释放。


实现中的特殊处理点


  1. 相较于系统KVO,如果不移除观察者,则会报异常(通知发送给 unsafe的指针,且观察者已经dealloc)。
    FBKVO 即使不移除观察者,也不会报出异常——原因上面FBKVO的优点3中已提到。其根本原因是,所有经过FBKVO添加的观察者其实都是添加的 _FBKVOSharedController的实例(即一个单例),对于单例来说,APP生命周期内是不会dealloc的。
  2. FBKVO中的观察者可以多次移除(在已经被移除的状态下,再此调用移除不会报异常)。
  3. FBKVO 中,如果不手动移除观察者,被观察者会在观察者将要dealloc的时候自动移除。


FBKVO中 _FBKVOSharedController 的一段特殊代码


_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中调用顺序如下,并对代码分析:

  1. -(void)observe:(id)object info:(nullable _FBKVOInfo *)info
    该方法执行到
    [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];然后执行下面2中方法。
  2. -(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。

  3. 紧接着继续执行1中未执行完的代码:
    而此时状态为_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