痛快的使用KVO ---- FBKVOController源碼分析

前言

KVO是iOS開發當中必不可少的一個工具,可以說是使用最廣泛的工具之一。無論你是要在檢測某一個屬性變化,還是構建viewmodel雙向綁定UI以及數據,KVO都是一個十分使用的工具。

然而??!

KVO用起來太TMD麻煩了,要注冊成為某個對象屬性的觀察者,要在適當的時候移除觀察者狀態,還要寫毀掉函數,更蛋疼的是對象屬性還要用字符串作為表示。其中任何一個地方都要注意很多點,而且因為Delegate回調函數的原因,導致代碼分離,可讀性極差,維護起來異常費勁。

所以說,對于我來說,能不用的時候,盡量繞過去用其他的方法,直到我發現了Facebook的開源框架KVOController


基本介紹

1、主要結構

屏幕快照 2017-07-19 上午12.51.20.png

事實上KVOController的實現只有2各類,第一個是NSObject的Category是我們使用的類,第二個則是具體的實現方法。

2、NSObject + FBKVOController 分析

在Category的.h文件中有兩個屬性,根據備注可知區別在意一個是持有的,另一個不是。

/**
 @abstract Lazy-loaded FBKVOController for use with any object
 @return FBKVOController associated with this object, creating one if necessary
 @discussion This makes it convenient to simply create and forget a FBKVOController, and when this object gets dealloc'd, so will the associated     controller and the observation info.
 */
@property (nonatomic, strong) FBKVOController *KVOController;

/**
 @abstract Lazy-loaded FBKVOController for use with any object
 @return FBKVOController associated with this object, creating one if necessary
 @discussion This makes it convenient to simply create and forget a FBKVOController.
 Use this version when a strong reference between controller and observed object would create a retain cycle.
 When not retaining observed objects, special care must be taken to remove observation info prior to deallocation of the observed object.
 */
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

Category的.m文件和其他文件類似,寫的都是setter以及getter方法,并且在getter方法中對別對兩個屬性做了對于 FBKVOController 的初始化。

- (FBKVOController *)KVOController
{
  id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey);

  // lazily create the KVOController
  if (nil == controller) {
    controller = [FBKVOController controllerWithObserver:self];
    self.KVOController = controller;
  }

  return controller;
}

- (void)setKVOController:(FBKVOController *)KVOController
 {
  objc_setAssociatedObject(self, NSObjectKVOControllerKey, KVOController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (FBKVOController *)KVOControllerNonRetaining
{
  id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);

  if (nil == controller) {
    controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
    self.KVOControllerNonRetaining = controller;
  }

  return controller;
}

- (void)setKVOControllerNonRetaining:(FBKVOController *)KVOControllerNonRetaining
{
  objc_setAssociatedObject(self, NSObjectKVOControllerNonRetainingKey, KVOControllerNonRetaining,     OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

3、FBKVOController分析

1)幾個基本API

/**
 @abstract Creates and returns an initialized KVO controller instance.
 @param observer The object notified on key-value change.
 @return The initialized KVO controller instance.
 */
+ (instancetype)controllerWithObserver:(nullable id)observer;


/**
 @abstract Registers observer for key-value change notification.
 @param object The object to observe.
 @param keyPath The key path to observe.
 @param options The NSKeyValueObservingOptions to use for observation.
 @param block The block to execute on notification.
 @discussion On key-value change, the specified block is called. In order to avoid retain loops, the block must avoid referencing the KVO controller or an owner thereof. Observing an already observed object key path or nil results in no operation.
 */
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;


/**
 @abstract Registers observer for key-value change notification.
 @param object The object to observe.
 @param keyPath The key path to observe.
 @param options The NSKeyValueObservingOptions to use for observation.
 @param action The observer selector called on key-value change.
 @discussion On key-value change, the observer's action selector is called. The selector provided should take the form of -propertyDidChange, -    propertyDidChange: or -propertyDidChange:object:, where optional parameters delivered will be KVO change dictionary and object observed. Observing nil or observing an already observed object's key path results in no operation.
 */
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action;


/**
 @abstract Block called on key-value change notification.
 @param observer The observer of the change.
 @param object The object changed.
 @param change The change dictionary which also includes @c FBKVONotificationKeyPathKey
 */
typedef void (^FBKVONotificationBlock)(id _Nullable observer, id object, NSDictionary<NSKeyValueChangeKey, id> *change);
  • 第一個很簡單了,是創建KVOController的實例
  • 第二個是注冊鍵值變化的觀察者,返回一個有固定參數的Block。需要注意的是,為了避免循環引用,盡量避免使用KVOController及其持有者。
  • 第三個和第二個一樣,也是注冊鍵值變化的觀察者,但是返回的是一個選擇子SEL,API介紹中還對選擇子SEL進行了建議。
  • 第四個很簡單,是第二個回調函數的Block。值得注意的是,observer以及object分別是變化的觀察者以及屬性變化的對象,所以我們書寫的時候可以改成我們需要的樣式,以此來免去另加的轉換過程。

主要的實現邏輯

KVOController的實現需要有兩個私有的成員變量:

  • NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
  • pthread_mutex_t _lock;

以及另一個暴露在外只讀的屬性:

  • @property (nullable, nonatomic, weak, readonly) id observer;

在實現過程中,作為 KVO 的管理者,其必須持有當前對象所有與 KVO 有關的信息,而在 KVOController 中,用于存儲這個信息的數據結構就是 NSMapTable。為了保證線程安全,需要持有pthread_mutex_t鎖,用于在操作NSMapTable時候使用。

1、下面讓我們看初始化方法:

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    _observer = observer;
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality :   NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

很簡單,主要工作是持有了傳進來的Observer,初始化了NSMapTable以及初始化了pthread_mutex_t鎖。
值得一提的是初始化** NSMapTable,我們回看第二部分,在屬性的區分就在于是否是持有,根據屬性的名字也能看出,不持有的話,引用計數就不會加一。所以在初始化的時候明顯的區分就是在創建NSPointerFunctionsOptions的時候,是StrongMemory還是WeakMemory。
通過方法
+ (instancetype)controllerWithObserver:(nullable id)observer**初始化的時候,默認為持有。

2、注冊觀察者

通常情況下我們會使用可以回調Block的API,但是也有少數情況下會選擇傳遞選擇子SEL的API,我們這里只拿傳遞Block的方法舉例子。

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }

  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

  // observe object with info
  [self _observe:object info:info];
}

在這里傳遞進來的一些參數會被封裝成為私有的_FBKVOInfo,那我們來簡單看一下_FBKVOInfo的主要實現:

{
@public
  __weak FBKVOController *_controller;
  NSString *_keyPath;
  NSKeyValueObservingOptions _options;
  SEL _action;
  void *_context;
  FBKVONotificationBlock _block;
  _FBKVOInfoState _state;
}

- (instancetype)initWithController:(FBKVOController *)controller
                           keyPath:(NSString *)keyPath
                           options:(NSKeyValueObservingOptions)options
                             block:(nullable FBKVONotificationBlock)block
                            action:(nullable SEL)action
                           context:(nullable void *)context
{
  self = [super init];
  if (nil != self) {
    _controller = controller;
    _block = [block copy];
    _keyPath = [keyPath copy];
    _options = options;
    _action = action;
    _context = context;
  }
  return self;
}

由此可以看出,** _FBKVOInfo的主要作用就是起到了一個類似Model一樣存儲主要數據的作用,并儲存了一個_FBKVOInfoState作為表示當前的 KVO 狀態。
需要注意的是,成員變量都是用了
@public修飾。
另外,對
- (NSString )debugDescription以及- (NSString )debugDescription兩個方法做了重寫,方便了使用以及Debug。

之后執行了私有方法*- (void)_observe:(id)object info:(_FBKVOInfo )info

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check for info existence
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    // observation info already exists; do not observe it again

    // unlock and return
    pthread_mutex_unlock(&_lock);
    return;
  }

  // lazilly create set of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);

  [[_FBKVOSharedController sharedController] observe:object info:info];
}

1)首先先進行的是對于自身持有的 _objectInfosMap這個成員變量的操作,一切都需要在先鎖定,執行結束再解鎖的過程。

  • 首先獲取了對于當前觀察者的注冊的關注列表。
  • 判斷是否當前需要關注的信息是否在此列表中,如果有則return出去,不再進行關注。
  • 如果當前的關注列表不存在則此時創建一個
  • 將關注的信息儲存在關注列表中。

2)然后是獲取了** _FBKVOSharedController單例并且執行了單例的- (void)observe:(id)object info:(nullable _FBKVOInfo )info*方法。

 - (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];
  }
}

加鎖,對于當前單例的NSHashTable進行添加操作的信息,并執行Foundation

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

然后對信息中的state進行更改。

3、觀察并回調

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                  ofObject:(nullable id)object
                    change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                   context:(nullable void *)context
{
  NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

  _FBKVOInfo *info;

  {
    // lookup context in registered infos, taking out a strong reference only if it exists
    pthread_mutex_lock(&_mutex);
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);
  }

  if (nil != info) {

     // take strong reference to controller
    FBKVOController *controller = info->_controller;
    if (nil != controller) {

      // take strong reference to observer
      id observer = controller.observer;
      if (nil != observer) {

        // dispatch custom block or action, fall back to default action
        if (info->_block) {
          NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
          // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
          if (keyPath) {
            NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
            [mChange addEntriesFromDictionary:change];
            changeWithKeyPath = [mChange copy];
          }
          info->_block(observer, object, changeWithKeyPath);
        } else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
          [observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
        } else {
          [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
        }
      }
    }
  }
}

這個就相對簡單了,主要是根據關注信息內是Block還是Action來執行,如果兩者都沒有就會調用觀察者 KVO 回調方法。

4、注銷觀察

事實上,注銷是在執行dealloc的時候執行的,同時也去掉了鎖:

- (void)dealloc
{
  [self unobserveAll];
  pthread_mutex_destroy(&_lock);
}

因為KVO事件都由私有的** _KVOSharedController** 來處理,所以當每一個** KVOController ** 對象被釋放時,都會將它自己持有的所有 KVO 的觀察者交由** _KVOSharedControlle** r的方法處理,我們再來看下代碼:

- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
  if (0 == infos.count) {
    return;
  }

  // unregister info
  pthread_mutex_lock(&_mutex);
  for (_FBKVOInfo *info in infos) {
    [_infos removeObject:info];
  }
  pthread_mutex_unlock(&_mutex);

  // remove observer
  for (_FBKVOInfo *info in infos) {
    if (info->_state == _FBKVOInfoStateObserving) {
      [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
    info->_state = _FBKVOInfoStateNotObserving;
  }
}

該方法會遍歷所有傳入的** _FBKVOInfo** ,從其中取出keyPath 并將 ** _KVOSharedController ** 移除觀察者。

當然,假如你需要手動的移除某一個的觀察者,** _KVOSharedController ** 也提供了方法:

- (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;
}

總結

這套框架提供了豐富的結構,基本能夠滿足我們對于KVO的使用需求。
只需要一次代碼,就可以完成對一個對象的鍵值觀測,同時不需要處理移除觀察者,也可以在同一處代碼進行鍵值變化之后的處理,從惡心的回調方法中解脫出來,不僅提供了使用方便,也不需要我們手動主要觀察者,避免了各種問題,絕對算的上一個完善好用的框架。


Refrence


另外

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容

  • KVO 作為 iOS 中一種強大并且有效的機制,為 iOS 開發者們提供了很多的便利;我們可以使用 KVO 來檢測...
    JzRo閱讀 949評論 0 2
  • KVO 作為 iOS 中一種強大并且有效的機制,為 iOS 開發者們提供了很多的便利;我們可以使用 KVO 來檢測...
    Draveness閱讀 6,909評論 11 59
  • 上半年有段時間做了一個項目,項目中聊天界面用到了音頻播放,涉及到進度條,當時做android時候處理的不太好,由于...
    DaZenD閱讀 3,034評論 0 26
  • FBKVOController是一個簡單易用的鍵值觀察框架,KVOController 對于 Cocoa 中 KV...
    我有小尾巴快看閱讀 1,395評論 0 0
  • 《沒蕃故人》張籍前年伐月支,城上沒全師。蕃漢斷消息,死生長別離。無人收廢帳,歸馬識殘旗。欲祭疑君在,天涯哭此時。 ...
    秋涼檸檬閱讀 834評論 0 1