KVOController詳解

KVO在MVC架構(gòu)的項目中是一種特別有用的技術(shù)。KVOController建立在Cocoa經(jīng)受時間考驗的KVO實現(xiàn)上。它提供簡單、現(xiàn)代的API,并且是線程安全的。優(yōu)點如下:

  • 通知是通過block、action或者NSKeyValueObserving回調(diào)(即-observeValueForKeyPath:ofObject:change:context)來實現(xiàn);
  • 不會出現(xiàn)移除observer的異常;
  • 在controller銷毀時隱式地移除observer;
  • 保證了線程安全,避免出現(xiàn)這樣的異常
    簡單地說,KVOController讓我們更優(yōu)雅、簡單、安全地使用KVO。

源碼分析

KVOController是面向觀察者設(shè)計的,而不是跟直接使用Cocoa的KVO時一樣面向被觀察者。這是一個很輕的開源庫,只由一個FBKVOController類和一個NSObject+FBKVOController分類構(gòu)成。

FBKVOController.h

FBKVOController類是用于管理整個KVO流程,它持有了觀察者對象,又提供了添加觀察行為的API,頭文件內(nèi)容如下:
初始化方法

/**
    @param observer 觀察者對象
    @param retainObserved 是否強引用被觀察對象
*/
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved;
/**
    簡便初始化方法。
    retainObserved默認為YES。
*/
- (instancetype)initWithObserver:(nullable id)observer;

公開屬性與API

/** 弱引用的方式持有觀察者對象 */
@property (nullable, nonatomic, weak, readonly) id observer;
/** 
    對指定對象的指定keyPath添加觀察,通過Block進行回調(diào)
    @param object 被觀察對象
    @param keyPath 被觀察對象的keyPath
    @param options NSKeyValueObservingOptions
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;
/**
    以SEL的方式回調(diào)
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action;
/**
    不注冊block和sel則回調(diào)觀察者類的-observeValueForKeyPath:ofObject:change:context:方法
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
/** 一次性監(jiān)聽多個keyPath */
- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;
- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options action:(SEL)action;
- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

/** 注銷觀察對象對應(yīng)的keyPath */
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath;
- (void)unobserve:(nullable id)object;
- (void)unobserveAll;

KVOController的API提供了三種回調(diào)的方式,也提供了一次性添加多個觀察的keyPaths的方法。此外,頭文件中還定義了兩個有意思的宏,用于判斷編譯時屬性是否存在(防止手滑寫錯),代碼如下:

#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))

因為我們平時直接在寫入keyPath時,都是以字符串的方式寫入,如果字符串拼寫錯誤的話可能會造成無法監(jiān)聽相應(yīng)屬性的問題。例如:

Person *person = [[Person alloc] init];
Observer *observer = [[Observer alloc] init];
[observer.KVOController observe:person
                        keyPath:@"fristNmae"
                        options:NSKeyValueObservingOptionNew
                          block:block];
person.firstName = @"西瓜冰";

把firstName寫錯成了fristNmae,因為屬性不存在,所以當屬性改變時沒有發(fā)生通知。使用宏FBKVOKeyPath后,我們可以跟調(diào)用對象屬性那樣將需要監(jiān)聽的屬性傳入,例如:

[observer.KVOController observe:person
                        keyPath:FBKVOKeyPath(person.firstName)
                        options:NSKeyValueObservingOptionNew
                          block:block];

因為有自動補全功能,所以一般不會寫錯,即使寫錯了,也會在編譯時報錯。這個宏的校驗步驟拆解后如下:

    // 1 校驗傳入的KeyPath是否有編譯錯誤
    ((void)(NO && ((void)KEYPATH, NO))
    // NO && ... 是為了運行時直接返回NO減少操作, 因為有(void)KEYPATH的存在,所以編譯時校驗了object.property
    
    // 2 將傳入的object.property轉(zhuǎn)換為"property"
    { const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; }
    // 2.1 #KEYPATH將object.property轉(zhuǎn)為字符串"object.property"
    // 2.2 strchr截取".property"
    // 2.3 NSCAssert保證點語法的存在
    // 2.4 ".property"+1="property"
    
    // 3 使用@()語法糖將char *轉(zhuǎn)換為NSString類型
    @(((void)NO, "property"))
    // 因為','操作符是返回后面的值,即string = (@"a", @"b");string的值為@"b"

宏FBKVOClassKeyPath也以差不多的形式實現(xiàn),就不重復(fù)了。

FBKVOController.m

FBKVOController的實現(xiàn)文件里面包含了兩個重要的私有類_FBKVOInfo_FBKVOSharedController。KVOController的全部功能就由這三個類來共同完成,這三個類的職責分別是:
_FBKVOInfo: 用來對每個被觀察的keyPath及對應(yīng)的options和回調(diào)(block或者SEL)進行了存儲。
FBKVOController: 將每個被觀察對象作為key值,將保存著該對象被觀察的keyPath及其對應(yīng)的回調(diào)的_FBKVOInfo的Set集合作為value值,通過一個NSMap?Table進行存儲。在添加觀察和移除觀察操作時,操作這個NSMap?Table,并且交付_FBKVOSharedController進行真正的KVO操作。
_FBKVOSharedController: 一個單例。所有的Cocoa的KVO事件都發(fā)生在這個單例對象里,這個對象是真正的觀察者。每次被監(jiān)聽對象的相關(guān)keyPath發(fā)生改變時,將會通知這個單例對象,再由這個單例對象通過_FBKVOInfo保存的信息來進行回調(diào)。
這三個對象的關(guān)系大概如上所述,接下來看看具體的代碼實現(xiàn),首先是最基礎(chǔ)的_FBKVOInfo:
屬性

@implementation _FBKVOInfo
{
@public
  __weak FBKVOController *_controller;
  NSString *_keyPath;
  NSKeyValueObservingOptions _options;
  SEL _action;
  void *_context;
  FBKVONotificationBlock _block;
  /** 標志的keyPath狀態(tài),分別為_FBKVOInfoStateInitial、_FBKVOInfoStateObserving、_FBKVOInfoStateNotObserving */
  _FBKVOInfoState _state;
}

_FBKVOInfo類的內(nèi)容大概就是由上面這些屬性,以及一系列初始化這些屬性的初始化方法構(gòu)成。此外還重寫了hash方法和isEqual,如下:

- (NSUInteger)hash
{
  return [_keyPath hash];
}

- (BOOL)isEqual:(id)object
{
  if (nil == object) {
    return NO;
  }
  if (self == object) {
    return YES;
  }
  if (![object isKindOfClass:[self class]]) {
    return NO;
  }
  return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}

因為一個被觀察對象的keyPath具有唯一性,為了防止對同一個對象重復(fù)添加了監(jiān)聽,所以_FBKVOInfo的唯一性由keyPath決定。
接下來是FBKVOController類,除了公開的屬性外還有以下私有屬性:
私有屬性

/** 以被觀察者對象為key,以_FBKVOInfo的Set作為value,來對其進行關(guān)聯(lián)和保存 */
NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
/** 用于保證NSMapTable線程安全的鎖 */
pthread_mutex_t _lock;

這里使用NSMutableSet來保存_FBKVOInfo是為了防止重復(fù)監(jiān)聽同一個被觀察對象的同一個keyPath。NSMutableSet的唯一性是通過調(diào)用_FBKVOInfohash方法和isEqual方法來確定的,就如上面所述,KVOController已經(jīng)重寫了_FBKVOInfohash方法和isEqual方法來保證keyPath的唯一性。
使用NSMapTable而不是使用NSMutableDictionary則是因為NSMapTable能控制對key和value的內(nèi)存管理方式。
初始化方法

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

初始化方法里,保存了觀察者對象,根據(jù)傳入的retainObserved設(shè)置NSMapTable管理內(nèi)存的方式,初始化了鎖。
接下來,以最方便的block方式回調(diào)為例,看一下KVOController的完整通知流程。
FBKVOController的方法

// 外部API接口,添加觀察的方法
- (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 *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
  
  [self _observe:object info:info];
}

// 嘗試從NSMapTable中取出已保存的_FBKVOInfo對象,有則返回,無則新增,并用鎖保證了存取過程的安全。
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  pthread_mutex_lock(&_lock);
  
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];
    
  _FBKVOInfo *existingInfo = [infos member:info];

  if (nil != existingInfo) {
    pthread_mutex_unlock(&_lock);
    return;
  }

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

  [infos addObject:info];

  pthread_mutex_unlock(&_lock);

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

該方法最后通過_FBKVOSharedController類的方法來添加真正的KVO監(jiān)聽。因為_FBKVOSharedController是個單例,所以第一次調(diào)用+sharedController會進行初始化,所以先看下_FBKVOSharedController的屬性和初始化,如下:
屬性

/** 使用NSHashTable來以弱引用的方式持有_FBKVOInfo */
NSHashTable<_FBKVOInfo *> *_infos;
/** 用于保證NSHashTable線程安全的鎖 */
pthread_mutex_t _mutex;

初始化方法

- (instancetype)init
{
  self = [super init];
  if (nil != self) {
    NSHashTable *infos = [NSHashTable alloc];
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
    _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
    if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
      _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
    } else {
      // silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
      _infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
    }

#endif
    pthread_mutex_init(&_mutex, NULL);
  }
  return self;
}

FBKVOController差不多的初始化方法,沒啥好說的。
_FBKVOSharedController的添加觀察方法

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

  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

  // 真正使用Cocoa的KVO添加觀察,以info作為context參數(shù)
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

  if (info->_state == _FBKVOInfoStateInitial) {
    info->_state = _FBKVOInfoStateObserving;
  } else if (info->_state == _FBKVOInfoStateNotObserving) {
    // 當NSKeyValueObservingOptions屬性中包含NSKeyValueObservingOptionInitial,
    // 并且在回調(diào)中取消了監(jiān)聽(調(diào)用unobserve方法)可能因為沒有移除監(jiān)聽導(dǎo)致出現(xiàn)安全問題。
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
}

_FBKVOSharedController的KVO監(jiān)聽方法

// KVO監(jiān)聽方法
- (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;
          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];
        }
      }
    }
  }
}

在這個監(jiān)聽方法中,_FBKVOSharedController將接收到的更改信息重新封裝后轉(zhuǎn)發(fā)給_FBKVOInfo保存的觀察者的對象。
從上面整個流程中,我們可以看到,觀察者對象并沒有真正地對被觀察者對象進行任何監(jiān)聽,而是通過一個專門負責觀察監(jiān)聽和轉(zhuǎn)發(fā)信息的單例類來完成監(jiān)聽和發(fā)送通知。這樣做的好處是為防止發(fā)送通知時觀察者對象未移除監(jiān)聽并且已經(jīng)不存在而導(dǎo)致應(yīng)用Crash的情況提供了雙層保障。因為該單例的在APP整個生命周期內(nèi)都存在,所以最多是接收到信息并不進行其他操作。最后,我們再看下KVOController怎么取消觀察:
FBKVOController取消觀察方法

- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath
{
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath];

  [self _unobserve:object info:info];
}

- (void)_unobserve:(id)object info:(_FBKVOInfo *)info
{
  pthread_mutex_lock(&_lock);

  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  _FBKVOInfo *registeredInfo = [infos member:info];

  if (nil != registeredInfo) {
    [infos removeObject:registeredInfo];

    if (0 == infos.count) {
      [_objectInfosMap removeObjectForKey:object];
    }
  }

  pthread_mutex_unlock(&_lock);

  [[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
}

差不多就是添加觀察的逆過程。
_FBKVOSharedController取消觀察方法

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

  pthread_mutex_lock(&_mutex);
  [_infos removeObject:info];
  pthread_mutex_unlock(&_mutex);

  if (info->_state == _FBKVOInfoStateObserving) {
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
  info->_state = _FBKVOInfoStateNotObserving;
}

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

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

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

這里取消對多個_FBKVOInfo的觀察的-unobserve:infos:方法,不是遍歷著調(diào)用-unobserve:info:,而是使用以上代碼實現(xiàn)是為了減少互斥鎖切換消耗的時間。
FBKVOController的dealloc方法

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

FBKVOController對象銷毀時會移除取消所有觀察。

NSObject+FBKVOController

分類NSObject+FBKVOController進一步簡化了我們的使用,這個分類提供了兩個屬性:

@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

這兩屬性都是懶加載的形式創(chuàng)建,區(qū)別在于是否強引用被觀察對象。

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

通過動態(tài)綁定的方式保存這兩屬性。

Reference

https://github.com/facebook/KVOController

本文作者:西瓜冰soso
本文鏈接:http://m.itdecent.cn/p/8deccb9c8398
溫馨提示:
由于本文是原創(chuàng)文章,可能會有更新以及修正一些錯誤,因此轉(zhuǎn)載請保留原出處,方便溯源,避免陳舊錯誤知識的誤導(dǎo)。另外文章如有錯誤,請不吝指教,謝謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容