首頁 > 軟體

Objective-C優雅使用KVO觀察屬性值變化

2022-08-08 18:00:37

引言

KVO 是蘋果為我們提供的一套強大的機制,用於觀察屬性值的變化,但是大家在日常開發中想必多少也感受到了使用上的一些不便利,比如:

  • 新增觀察者和移除觀察者的次數需要一一對應,否則會 Crash
  • 新增觀察者和接受到屬性變更通知的位置是分開的,不利於判斷上下文。
  • 多次對同一個屬性值進行觀察,會觸發多次回撥,影響業務邏輯。

為了解決上述三個問題,業界提出了一些方便開發者的開源方案,我們一起來看一下。

KVOController

KVOController 建立在 Cocoa 久經考驗的 KVO 實現之上。它提供了一個簡單、現代的 API,也是執行緒安全的。好處包括:

  • 使用 blockscustom actionsNSKeyValueObserving 回撥。
  • 觀察者移除沒有異常。
  • 控制器 dealloc 時隱式移除觀察者。
  • 具有防止觀察者復活的特殊保護的執行緒安全。

其使用方式也很簡單:

// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;
// observe clock date property
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
  // update clock view with new value
  clockView.date = change[NSKeyValueChangeNewKey];
}];

同時,KVOController 還提供了分類,通過關聯參照自動幫你建立了 KVOController 框架,方便我們使用:

[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];

我們來簡單看一下 KVOController 是怎麼做的:

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

KVOController 分為兩種:強參照和弱參照,其中強參照會在使用時持有被觀察的物件,反之弱參照則不會。所以在初始化的時候,會建立一個 objectInfosMap,這個是 NSMapTable,支援弱參照容器。同時會建立一個鎖。

註冊觀察者的時候的程式碼如下:

- (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 物件,來實現對觀察者資訊的封裝,算是一個模型類,這個內部類的初始化方法如下:

- (instancetype)initWithController:(FBKVOController *)controller keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  return [self initWithController:controller keyPath:keyPath options:options block:block action:NULL context:NULL];
}
- (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;
}

接下來會將觀察者的資訊儲存到 KVOController 建立時初始化的 NSMapTable 中:

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

objectInfosMap 是一個 NSMapTable 物件,使用被觀察的物件 object 作為 key, NSMutableSet 作為 value,如果已經有 info 存在了,不會進行二次觀察。集合儲存自定義物件需要判斷其 hash 值,_FBKVOInfohash 方法實現如下:

- (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 構成了觀察的唯一性。

接下來來看 _FBKVOSharedController 如何進行的觀察:

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

_FBKVOSharedController 會將 _FBKVOInfo 儲存到一個 NSHashTable 物件中,並對其進行 KVO

在接受到回撥時的處理如下所示:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSString *, 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<NSString *, 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];
        }
      }
    }
  }
}

就是根據在 _FBKVOInfo 中儲存的資訊,進行相應的回撥。

在持有 KVOController 的物件被銷燬的時候,KVOController 也會相應的取消對所有觀察物件的 KVO 防止出現 Crash

- (void)dealloc
{
  [self unobserveAll];
  pthread_mutex_destroy(&_lock);
}
- (void)unobserveAll
{
  [self _unobserveAll];
}
- (void)_unobserveAll
{
  // lock
  pthread_mutex_lock(&_lock);
  NSMapTable *objectInfoMaps = [_objectInfosMap copy];
  // clear table and map
  [_objectInfosMap removeAllObjects];
  // unlock
  pthread_mutex_unlock(&_lock);
  _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];
  for (id object in objectInfoMaps) {
    // unobserve each registered object and infos
    NSSet *infos = [objectInfoMaps objectForKey:object];
    [shareController unobserve:object infos:infos];
  }
}

需要注意的是,使用 KVOController 觀察自身屬性的時候,會出現記憶體洩露的情況,這種情況下請記得使用 KVOControllerNonRetaining 來進行觀察,同時在觀察者 dealloc 的時候,呼叫 unobserveAll 方法。

YYCategories

很多時候是否引入一個第三方庫不是我們業務開發能決定的,而你又想在開發時安全方便的使用 KVO,你可以參考 YYCategories 裡提供的方案來做,使用方法如下:

[self.person addObserverBlockForKeyPath:@"age" block:^(id  _Nonnull obj, id  _Nonnull oldVal, id  _Nonnull newVal) {
    NSLog(@"oldVal: %@, newVal: %@", oldVal, newVal);
}];

其實現原理也很簡單,通過關聯物件設定一個 NSMutableDictionary,這個字典以 keyPathkey,與這個 key 有關的所有 block 組成的可變陣列為 value

// 新增 `KVO`
- (void)addObserverBlockForKeyPath:(NSString *)keyPath block:(void (^)(__weak id obj, id oldVal, id newVal))block {
    if (!keyPath || !block) return;
    _YYNSObjectKVOBlockTarget *target = [[_YYNSObjectKVOBlockTarget alloc] initWithBlock:block];
    NSMutableDictionary *dic = [self _yy_allNSObjectObserverBlocks];
    NSMutableArray *arr = dic[keyPath];
    if (!arr) {
        arr = [NSMutableArray new];
        dic[keyPath] = arr;
    }
    [arr addObject:target];
    [self addObserver:target forKeyPath:keyPath options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
}
// 根據 `keyPath` 移除 `KVO`
- (void)removeObserverBlocksForKeyPath:(NSString *)keyPath {
    if (!keyPath) return;
    NSMutableDictionary *dic = [self _yy_allNSObjectObserverBlocks];
    NSMutableArray *arr = dic[keyPath];
    [arr enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) {
        [self removeObserver:obj forKeyPath:keyPath];
    }];
    [dic removeObjectForKey:keyPath];
}
// 移除 `KVO`
- (void)removeObserverBlocks {
    NSMutableDictionary *dic = [self _yy_allNSObjectObserverBlocks];
    [dic enumerateKeysAndObjectsUsingBlock: ^(NSString *key, NSArray *arr, BOOL *stop) {
        [arr enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) {
            [self removeObserver:obj forKeyPath:key];
        }];
    }];
    [dic removeAllObjects];
}
// 獲取當前註冊的所有 `KVO` `Block`
- (NSMutableDictionary *)_yy_allNSObjectObserverBlocks {
    NSMutableDictionary *targets = objc_getAssociatedObject(self, &block_key);
    if (!targets) {
        targets = [NSMutableDictionary new];
        objc_setAssociatedObject(self, &block_key, targets, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return targets;
}

而通知的回撥則是放在 _YYNSObjectKVOBlockTarget 中的:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (!self.block) return;
    BOOL isPrior = [[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue];
    if (isPrior) return;
    NSKeyValueChange changeKind = [[change objectForKey:NSKeyValueChangeKindKey] integerValue];
    if (changeKind != NSKeyValueChangeSetting) return;
    id oldVal = [change objectForKey:NSKeyValueChangeOldKey];
    if (oldVal == [NSNull null]) oldVal = nil;
    id newVal = [change objectForKey:NSKeyValueChangeNewKey];
    if (newVal == [NSNull null]) newVal = nil;
    self.block(object, oldVal, newVal);
}

不過從原始碼上看,還是需要自己在 dealloc 的時候移除觀察者的,不過這種方案的好處是可以多次監聽同一個 keyPath,實現真正的一對多(雖然好像沒啥荷包蛋用)。

以上就是Objective-C優雅使用KVO觀察屬性值變化的詳細內容,更多關於Objective-C KVO觀察屬性值的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com