教你一行程式碼使用 KVO(Facebook 出品 FBKVOController 原始碼使用及解讀)

鄭一一發表於2017-12-27

前言

進入 iOS 開發一年多,大部分時間都在寫業務程式碼,鮮有對優秀開原始碼的學習、總結。深知,是時候開始學習一些。萬事開頭難,所以我準備從比較簡短的開原始碼開始學習。第一篇準備寫寫 Facebook 這個極度熱愛開源的公司的一套關於 KVO 的開原始碼——FBKVOController。閱讀本篇文章前,希望你對 KVO 已經有一定的瞭解。

正文

先說說本文主要想講一下哪些東西。

概述

  • FBKVOController 做了什麼
  • FBKVOController 使用姿勢
  • FBKVOController 原始碼解析
  • FBKVOController 設計思路總結
  • FBKVOController 其它收穫

FBKVOController 做了什麼?

簡單來說,Facebook 開源的這套程式碼,主要是對我們經常使用的 KVO 機制進行了額外的一層封裝。其中最亮眼的特色是提供了一個 block 回撥讓我們進行處理,避免 KVO 的相關程式碼四處散落,不再需要使用下面這個方法:


- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
複製程式碼

使用姿勢

利用開源框架,我們這樣實現,其中第二種方法可以用一行程式碼實現 KVO

#import "ViewController.h"
#import "FBKVOController.h"
#import "NSObject+FBKVOController.h"

@interface KVOModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end

@implementation KVOModel
@end

NS_ASSUME_NONNULL_BEGIN

@interface ViewController ()
@property (nonatomic, strong) KVOModel *model;
@property (nonatomic, strong) FBKVOController *kvoController;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //建立被觀察的 model 類
    KVOModel *model = [[KVOModel alloc] init];
    //初始化設定 model 的成員變數值
    model.name = @"wo";
    model.age = 5;
    self.model = model;
    
    //第一種方法:建立 FBKVOController 物件,並被 VC 強引用,否則出了當前作用域,就會被銷燬
    FBKVOController *kvoController = [[FBKVOController alloc] initWithObserver:self];
    _kvoController = kvoController;
   
    //新增 觀察
    [kvoController observe:model keyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
            NSLog(@"我的舊名字是:%@", change[NSKeyValueChangeOldKey]);
            NSLog(@"我的新名字是:%@", change[NSKeyValueChangeNewKey]);
    }];

    //第二種方法:無需主動建立 FBKVOController 物件,self.KVOController 直接懶載入建立FBKVOController 物件
    //可以直接對某個物件的多個成員變數執行 KVO
    //------真正實現一行程式碼搞定 KVO------
    [self.KVOController observe:model keyPaths:@[@"name", @"age"] options:  NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
        
        NSString *changedKeyPath = change[FBKVONotificationKeyPathKey];
        

        if ([changedKeyPath isEqualToString:@"name"]) {
            NSLog(@"修改了名字");
        } else if ([changedKeyPath isEqualToString:@"age"]) {
            NSLog(@"修改了年齡");
        }
        
        NSLog(@"舊值是:%@", change[NSKeyValueChangeOldKey]);
        NSLog(@"新值是:%@", change[NSKeyValueChangeNewKey]);
    }];

    //修改 model 的 name 成員變數
    model.name = @"ni";
}

@end

NS_ASSUME_NONNULL_END
複製程式碼

相比於原生 API 優勢:

  • 1 可以以陣列形式,同時對 model 的多個 不同成員變數進行 KVO
  • 2 利用提供的 block,將 KVO 相關程式碼集中在一塊,而不是四處散落。比較清晰,一目瞭然。
  • 3 不需要在 dealloc 方法裡取消對 object 的觀察,當 FBKVOController 物件 dealloc,會自動取消觀察。

原始碼解析

這套原始碼主要包括了FBKVOController.hFBKVOController.mNSObject+FBKVOController.hNSObject+FBKVOController.m四個檔案。 其中,NSObject+FBKVOController 這個分類比較簡單。它主要乾的事是通過 objc_setAssociatedObject (關聯物件),以懶載入的形式給 NSObject ,建立並關聯一個 FBKVOController 的物件。 接下來,我會著重介紹一下今天的主角 FBKVOController類。其檔案中還包含另外兩個類,_FBKVOInfo_FBKVOSharedController 。下面都會介紹到。 先來看看 FBKVOController 指定初始化函式:


- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    //一般情況下 observer 會持有 FBKVOController 為了避免迴圈引用,此處的_observer 的記憶體管理語義是弱引用
    _observer = observer;
    //定義 NSMapTable key的記憶體管理策略,在預設情況,傳入的引數 retainObserved = YES
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    //建立 NSMapTable  key 為 id 型別,value 為 NSMutableSet<_FBKVOInfo *> 型別
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    //初始化互斥鎖,避免多執行緒間的資料競爭
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}
複製程式碼

以上初始化程式碼中,註釋都寫得比較清楚了。唯一比較陌生的是 NSMapTable 。簡單來說,它與 NSDictionary 類似。不同之處是 NSMapTable 可以自主控制 key / value 的記憶體管理策略。而 NSDictionary 的記憶體策略是固定為 copy。當 key 為 object 時, copy的開銷可能比較大!因此,在這裡只能使用相對比較靈活的 NSMapTable

執行 KVO 的相關方法程式碼解析


- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
    //當 keyPath 字串長度為 0 或者 block 為空時,會產生斷言,程式會 crash
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
    //如果 “被觀察物件” 為 nil,同樣會直接返回
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }

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

  // observe object with info (利用儲存的資訊對 “被觀察物件” 進行觀察!)
  [self _observe:object info:info];
}
複製程式碼

上述程式碼中,出現了一個前面提及到的 _FBKVOInfo 類,其儲存的資訊包括了 FBKVOControllerkeypathoptionsblock

接上段程式碼的最後一句 [self _observe:object info:info];


- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock 互斥鎖加鎖
  pthread_mutex_lock(&_lock);
  //還記得初始化 FBKVOController 時建立的 NSMapTable 麼?
  //其結構是以 被觀察者 object 為 key。並不像我們常用的 NSDictionary 那樣是以 NSString 為 key
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check for info existence 
  // 必須重寫 _FBKVOInfo hash 以及 isEqual 方法,這樣才能使用 NSSet 的 member 方法。
  _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;
  }

    //如果沒有 關於這個 object(被觀察者)的相關資訊,則建立 NSMutableSet,並新增到 NSMapTable 中
  // lazilly create set of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve -- NSMutableSet 加 info
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);
    
    //sharedController 是 幹嘛的?  將所有觀察資訊統一交由一個單例來完成
  [[_FBKVOSharedController sharedController] observe:object info:info];
}
複製程式碼

總結一下上面一段的資料結構。FBKVOController 擁有成員變數 NSMapTableNSMapTable被觀察者(object)為 key, NSMutableSet 為 value 。在 NSMutableSet 中,儲存了不同 info。其關係圖如下圖:

FBKVOController.png
追蹤一下這句程式碼

[[_FBKVOSharedController sharedController] observe:object info:info];
複製程式碼

_FBKVOSharedController 是會在 app 生命週期一直存在的單例,其職責是:接收並轉發 KVO 通知。因此 app 當中所有 KVO 的通知都是由這個單例來完成的。


- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return;
  }

  // register info   向 NSHashTable 新增 info
  //注意:在 _FBKVOController 類中的 NSMutableSet 已經強引用了 info
  //這裡是為了弱引用 info,才使用 NSHashTable,當 info dealloc 時,同時會從容器中刪除
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

    //_FBKVOSharedController 是實際的觀察者! 隨後會進行轉發 ,
   //context 是 void * 無型別指標,是 info 的指標!
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
    //如果 state 是原始狀態,則改為正在觀察的狀態,表明是在正在觀察的狀態
  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];
  }
}
複製程式碼

以上程式碼中想單獨說一下下面的程式碼,其中的 context 引數使用的是 (void *)info 的指標,這樣可以保證 context 的唯一性。

接收 KVO 通知,並做相應處理


- (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
    // 利用 context 查詢 info,其中用到了 void  * 轉換為 id 型變數 (__bridge id)
    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];
              //字典合併,並重新拷貝一份,
              //包含資訊有:1、改變了哪個值 mChange 2、 原先的 change 字典
            [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];
        }
      }
    }
  }
}
複製程式碼

設計思路總結

  • 1 FBKVOController 持有 NSMapTable,以 objectkey 得到相對應的 NSMutableSetNSMutableSet 中儲存了不同的 _FBKVOInfo。這套資料結構的主要作用是防止開發人員重複新增相同的 KVO。當檢查到其中已存在相同的 _FBKVOInfo 物件時,不再執行後面的程式碼。
  • 2 _FBKVOSharedController 持有 NSHashTableNSHashTable 以弱引用的方式持有不同的 _FBKVOInfo。此處實際執行 KVO 程式碼。_FBKVOInfo 有一個重要的成員變數 _FBKVOInfoState,根據這個列舉值(_FBKVOInfoStateInitial_FBKVOInfoStateObserving_FBKVOInfoStateNotObserving) 來決定新增或者刪除 KVO

收穫(通讀、研究原始碼後)

  • 1 NSSet / NSHashTableNSDictionary/ NSMapTable 的學習
    • NSSet 是過濾掉重複 object 的集合類,NSHashTableNSSet 的升級版容器,並且只有可變版本,允許對新增到容器中的物件是弱引用的持有關係, 當NSHashTable 中的物件銷燬時,該物件也會從容器中移除。
    • NSMapTableNSDictionary 類似,唯一區別是多了個功能:可以設定 keyvalueNSPointerFunctionsOptions 特性! NSDictionarykey 策略固定是 copy,考慮到開銷問題,一般使用簡單的數字或者字串為 key。但是如果碰到需要用 object 作為 key 的應用場景呢?NSMapTable 就可以派上用場了!可以通過 NSFunctionsPointer 來分別定義對 keyvalue 的記憶體管理策略,簡單可以分為 strong,weak以及 copy
  • 2 幾個比較有用的巨集
    • NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END,如果需要每個屬性或每個方法都去指定 nonnullnullable,是一件非常繁瑣的事。蘋果為了減輕我們的工作量,專門提供了這兩個巨集。在這兩個巨集之間的程式碼,所有比較簡單指標物件都被假定為 nonnull,因此我們只需要去指定那些 nullable 的指標。如果我們強行通過點語法將一個非空指標置空,編譯器會報 warning
    • NS_UNAVAILABLE 當我們不想要其他開發人員,用普通的 init 方法去初始化一個類,我們可以在.h 檔案裡這樣寫: - (instancetype)init NS_UNAVAILABLE; 編譯器不但不會提示補全 init 方法,就算開發人員強制傳送 init 訊息,編譯器會直接報錯。
    • NS_DESIGNATED_INITIALIZER 指定的初始化方法。當一個類提供多種初始化方法時,所有的初始化方法最終都會呼叫這個指定的初始化方法。比較常見的有: - (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
  • 3 斷言的使用 NSAssert(x,y);xBOOL 值,y 為 字串型別。當 x = YES,則不產生斷言。當 x = NO,則產生斷言,app 會 crash,並在控制檯中列印 y 字串內容。合理利用斷言,可以保證 app 的健壯性。
  • 4 互斥鎖的使用
    • pthread_mutex_init(&_lock, NULL);(初始化)&_lock 是互斥鎖的指標,第二個引數是互斥鎖的屬性。預設值是:當一個執行緒加鎖以後,其餘請求鎖的執行緒將形成一個等待佇列,並在解鎖後按優先順序獲得鎖。這種鎖策略保證了資源分配的公平性。
    • pthread_mutex_destroy(&_lock);(銷燬)
    • pthread_mutex_lock(&_lock);(加鎖)
    • pthread_mutex_unlock(&_lock);(解鎖)
    • 涉及到資料的讀寫操作時,都需要加鎖來保證避免資料競爭。
    • 順便複習一下死鎖的概念:如果執行緒A鎖住了記錄1並等待記錄2,而執行緒B鎖住了記錄2並等待記錄1,這樣兩個執行緒就發生了死鎖現象。

小尾巴

第一次寫原始碼解析,感覺思路都還比較混亂,認識也還比較淺薄,需要逐漸摸索一下。有什麼問題歡迎提給我!

一些相關知識的連結 NSHashTable的特性和使用 [互斥鎖](http://blog.csdn.net/yasi_xi/article/details/19112077)

相關文章