Apple官文中的KVO 與 FBKVOController

前言


本文將主要介紹如下內容:
html

  • 詳細列出Apple官文中KVO的注意事項(Apple KVO相關的引用皆摘自Apple官文)。
  • 介紹FBKVOController,以及它如何避免系統提供的KVO坑點。

Apple官文中的KVO


關於KVO

 

官方文檔:git

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 Guidegithub


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


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.觀察者監聽方法可能帶來的異常


一般,爲了監聽變化,將實現該方法:
ide

- (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 的朋友自行查閱下,這裏很少介紹了)

關於原理描述可參考下圖理解:





KVO的詳細原理


雖然沒有Apple的源碼,但若是有同窗真想要詳細瞭解KVO的實現原理,可參考Github上的 Aspect 開源庫,其源碼會給你答案。


FBKVOController




FBKVOController源碼比較簡單,感興趣的可閱讀 FBKVOController Github地址,如下主要講下部分細節與你們可能漏掉的點。

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

相關文章
相關標籤/搜索