FBKVOController原始碼剖析與學習

Dwyane_Coding發表於2018-02-04

原始碼剖析學習系列:(不斷更新)

1、FBKVOController原始碼剖析與學習
2、MJRefresh原始碼剖析與學習
3、YYImage原始碼剖析與學習


FBKVOController是對KVO的封裝,本文會分為兩大部分:

一、針對FBKVOController進行原始碼解讀,剖析其封裝思路

二、針對原始碼,抽取其精要,模仿學習,變為己用

優勢

相對於原生 API 優勢

1、可以以陣列形式,同時對 model 的多個 不同成員變數進行 KVO。

2、利用提供的 block,將 KVO 相關程式碼集中在一塊,而不是四處散落。比較清晰,一目瞭然。

3、不需要在 dealloc 方法裡取消對 object 的觀察,當 FBKVOController 物件 dealloc,會自動取消觀察。

使用

//1、在當前類建立一個KVO的控制器,並且指明監聽者為當前類
// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;

//2、監聽物件
// observe clock date property
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
  // 更新UI
  // update clock view with new value
  clockView.date = change[NSKeyValueChangeNewKey];
}];

複製程式碼

使用步驟很簡短,我們關鍵是理解裡面的封裝。

FBKVOController

一、我們先看一下建立KVO controller例項的方法,以及銷燬方法--(生命週期)

#pragma mark Lifecycle - 
//1、
+ (instancetype)controllerWithObserver:(nullable id)observer
{
  return [[self alloc] initWithObserver:observer];
}
//2、初始化observer,並依據retainObserved值決定記憶體策略
- (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;
}
//3、
- (instancetype)initWithObserver:(nullable id)observer
{
  return [self initWithObserver:observer retainObserved:YES];
}
//4、在dealloc登出所有監聽並且銷燬上面的互斥鎖
- (void)dealloc
{
  [self unobserveAll];
  pthread_mutex_destroy(&_lock);
}
複製程式碼

總結:1、NSPointerFunctionsStrongMemory建立了一個retain/release物件的集合,非常像常規的NSSet或NSArray。 NSPointerFunctionsWeakMemory使用等價的__weak來儲存物件並自動移除被銷燬的物件。

2、比較陌生的是 NSMapTable 。簡單來說,它與 NSDictionary 類似。不同之處是 NSMapTable 可以自主控制 key / value 的記憶體管理策略。而 NSDictionary 的記憶體策略是固定為 copy。當 key 為 object 時, copy 的開銷可能比較大!因此,在這裡只能使用相對比較靈活的 NSMapTable。具體可以移步關於 NSMapTable

3、pthread_mutex:這是一種超級易用的互斥鎖,使用的時候,只需要初始化一個 pthread_mutex_t,用 pthread_mutex_lock 來鎖定, pthread_mutex_unlock 來解鎖,當使用完成後,記得呼叫 pthread_mutex_destroy 來銷燬鎖

二、接下來看一下注冊監聽物件的方法

- (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;
  }
  //建立FBKVOInfo
  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
  
  //利用FBKVOInfo觀察物件
  // observe object with info
  [self _observe:object info:info];
}

- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  NSAssert(0 != keyPaths.count && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPaths, block);
  if (nil == object || 0 == keyPaths.count || NULL == block) {
    return;
  }
//遍歷每個keyPath,再遞迴
  for (NSString *keyPath in keyPaths) {
    [self observe:object keyPath:keyPath options:options block:block];
  }
}
複製程式碼

使用斷言,提示使用者缺少必要引數; 為了避免保留迴圈,該block必須避免引用KVO控制器或其所有者。觀察已經觀察到的物件keyPath或nil的結果是沒有操作的。

看一下FBKVOInfo的init方法

- (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;
}
複製程式碼

重寫init方法,把值分別賦值給屬性,對於為什麼要if (nil != self),我認為,當應用程式在更有限的記憶體中執行,這是一個傳統的編碼建議。具體請看各位大神的回答--> In Objective-C why should I check if self = [super init] is not nil?

看一下觀察FBInfo的方法

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);
//1
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];
//2
  // 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;
  }
//3
  // 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);
//4
  [[_FBKVOSharedController sharedController] observe:object info:info];
}
複製程式碼

NSMutableSet是一個集合,它有幾個特點: 1、沒有順序,所有元素並非按照加入順序排列 2、重複元素只會新增一個,因此不用擔心裡面的元素有重複

NSMapTable是比Dicitionary更強大的一個類。我們定義一個Person類,用來記錄人名,我們再建立一個Favourite類用來建立愛好物件,現在有Rose和Jack兩個人,分別的愛好是ObjC和Swift,人和愛好必須要用物件實現,而且必須關聯起來在一個表裡,以便我們進行查詢和記錄。如果是以前的話需要自己建立一個Dictionary,把人名的name欄位作為key,favourite的物件作為value。但是這樣有一個問題,如果突然某一天,我Person裡面增加了個欄位age,我這個表還要記錄每個人的年齡,供我以後來查詢不同年齡段的人統計使用呢?這下就很尷尬了,因為Dicitionary沒辦法實現我們要的這個效果,不過沒關係NSMapTable可以實現,詳細請移步關於 NSMapTable

1、根據被觀察的object獲取其對應的infos set。這個主要作用在於避免多次對同一個keyPath新增多次觀察,避免crash。因為每呼叫一次addObserverForKeyPath就要有一個對應的removeObserverForKey。

2、從infos set判斷是不是已經觀察此次info了,避免重複觀察。

3、如果infos為空,就把object當做Key、infos當做Object存入 NSMapTable,[infos addObject:info];再把info與infos關聯起來。這裡聽起來可能有點彆扭,我做個比喻:object是上面所說的是Rose,infos愛好ObjC,而info則是他的age

4、使用了單例,將觀察的資訊及關係註冊到_FBKVOSharedController中,並且呼叫iOS自帶的KVO方法觀察

_FBKVOSharedController作為一個傳達者,用來接收和轉發KVO通知

- (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);
  //1
  // 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];
  }
}
複製程式碼

根據info的狀態來選擇新增或移除觀察者

1、代表所有的觀察資訊都首先由FBKVOSharedController進行接收,隨後進行轉發。

//當屬性的值發生變化時,自動呼叫此係統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
    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];
        }
      }
    }
  }
}
複製程式碼

根據info的block回撥或者actioin等等進行訊息轉發。

至此,對FBKVOController的原始碼剖析基本結束,下面是剖析後的學習

學習

1、NSHashTable
+ (instancetype)personWithName:(NSString *)name
{
    DWPerson *person = [[DWPerson alloc] init];
    person.name = name;
    //1、待會替換
    person.family = [[NSMutableArray alloc] init];
    [person.family addObject:person];
    return [person autorelease];
}
- (NSString *)description
{
    return [NSString stringWithFormat:@"%@'s retainCount is %lu",self.name,[self retainCount]];
}
- (void)dealloc
{
    self.name = nil;
    self.family = nil;
    [super dealloc];
}
複製程式碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        DWPerson *person_1 = [DWPerson personWithName:@"iOS"];
        DWPerson *person_2 = [DWPerson personWithName:@"swift"];
        DWPerson *person_3 = [DWPerson personWithName:@"android"];
        DWPerson *person_4 = [DWPerson personWithName:@"java"];
        DWPerson *person_5 = [DWPerson personWithName:@"ruby"];
        
        id list = @[person_1, person_2, person_3, person_4, person_5];
        NSLog(@"%@",list);

    }
    return 0;
}
複製程式碼

列印:

( "iOS's retainCount is 3", "swift's retainCount is 3", "android's retainCount is 3", "java's retainCount is 3", "ruby's retainCount is 3" )

可以看出每個person的retainCount為3,因為family持有person,person持有family,如果我們運用NSHashTable,則可以完美解決此問題

我們替換1中的程式碼,

+ (instancetype)personWithName:(NSString *)name
{
    DWPerson *person = [[DWPerson alloc] init];
    person.name = name;
    person.family = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
    [person.family addObject:person];
    return [person autorelease];
}
複製程式碼

列印:

( "iOS's retainCount is 2", "swift's retainCount is 2", "android's retainCount is 2", "java's retainCount is 2", "ruby's retainCount is 2" ) 可看出,已解決迴圈引用

2、巨集定義魔法

先看一下系統的KVO方法

[testPerson addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
複製程式碼

這樣寫keyPath,如果age屬性不存在,也不會告知,導致後續的排查困難,但這種低階錯誤在FBKVOController不復存在,因為其使用了巨集定義

FBKVOController中的巨集定義

#define FBKVOKeyPath(KEYPATH) \
@(((void)(NO && ((void)KEYPATH, NO)), \
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; })))

#define FBKVOClassKeyPath(CLASS, KEYPATH) \
@(((void)(NO && ((void)((CLASS *)(nil)).KEYPATH, NO)), #KEYPATH))
複製程式碼

該巨集定義使用了C語言的逗號表示式,(3+5,6+8)稱為逗號表示式,其求解過程先表示式1,後表示式2,整個表示式值是表示式2的值,如:(3+5,6+8)的值是14,a=(a=3 x 5,a x 4)的值是60,而(a=3 x 5,a x 4)的值是60, a的值是15。

使用逗號表示式,我覺得主要是為了FBKVOClassKeyPath

FBKVOClassKeyPath(DWPerson, name)==(((void)(NO && ((void)((DWPerson *)(nil)).name, NO)), #KEYPATH)),其會檢查DWPerson中是否有name屬性

3、自釋放

FBKVOController通過自釋放的機制來實現observer的自動移除,其實就是給observer的類中新增一個FBKVOController的成員變數,然後在FBKVOController中的dealloc移除observer,下面是個例子

#import "DWTestViewController.h"
#import "DWObserViewController.h"

@interface DWTestViewController ()
@property (nonatomic, strong) DWObserViewController *obserVC;
@end

@implementation DWTestViewController

- (instancetype)init
{
    self = [super init];
    if (nil != self) {
        _obserVC = [[DWObserViewController alloc] init];
        NSLog(@"DWTestVC建立");
        NSLog(@"DWObserVC建立");
    }
    return self;
}
複製程式碼
#import "DWObserViewController.h"

@implementation DWObserViewController

- (void)dealloc {
    NSLog(@"DWObserVC跟著銷燬");
}
複製程式碼

列印:

2018-02-05 15:32:39.299859+0800 FBKVOController_Demo[6804:208216] DWTestVC建立 2018-02-05 15:32:39.300209+0800 FBKVOController_Demo[6804:208216] DWObserVC建立 2018-02-05 15:32:41.271585+0800 FBKVOController_Demo[6804:208216] DWTestVC銷燬 2018-02-05 15:32:46.520148+0800 FBKVOController_Demo[6804:208216] DWObserVC跟著銷燬

參考:

NSHashTable和NSMapTable用法

相關文章