iOS实用功能

FBKVOController源码解析

2020-05-05  本文已影响0人  某某香肠

在平常iOS开发中,KVO是比较常用的,但是系统提供的KVO有一些坑,主要体现在

  1. 观测的属性要用字符串定义,编译器不会做检查,此外之后项目对属性的重命名也不会影响更改这个字符串导致未知bug
  2. 在同一个类监听多个属性的时候,其kvo的回调统一在-[NSObject observeValueForKeyPath:ofObject:change:context:]里面,写代码的时候只能通过keyPathobject写一堆ifelse来区分
  3. 释放观察者的时候需要移除观察者,而且不能过度地移除观察者,需要保持添加观察者次数=移除观察者次数,否则会crash

这些问题,FBKVOController进行有效的解决。用这个库作KVO的调用如下:

    [self.KVOController observe:self.label keyPath:FBKVOKeyPath(_label.text) options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        NSLog(@"1");
    }];

首先对字符串的问题,它有个FBKVOKeyPath这个宏进行封装,宏里面的属性是可以通过编译器检查的,另外它的回调是一个block,可读性会更好,此外还能选择-[FBKVOController observe:keyPaths:options:action:]-[FBKVOController observe:keyPaths:options:context:]这些方法进行监听,非常灵活。它也解决了不移除观察者会崩溃的问题,开发者无需手动移除观察者,内部已做处理。

FBKVOKeyPath

可以在FBKVOController.h这个地方看到这个宏:

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

其是跟extobjc的宏差不多,都是通过strchr这个函数找到.的位置,然后从其后一位开始读,需要注意的是它找的是第一个点的位置,也就是说FBKVOKeyPath(self.label.text)得出的最终结果是@"label.text",会跟预想的不一样。

NSObject+FBKVOController.h

@interface NSObject (FBKVOController)

@property (nonatomic, strong) FBKVOController *KVOController;

@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

@end

首先对NSObject扩展了两个属性(用关联对象的方式),其中KVOController是会持有监听的对象,也就是说被观察者会一直被观察者间接持有着,如果监听的对象恰好是观察者自身或者持有观察者就会形成循环引用。

如果使用KVOControllerNonRetaining,则不会造成上述的循环引用,但是被观察者可能比观察者提前释放掉(如果是上述情况则不会),这个时候需要外部移除观察者(测试一下不移除并不会崩溃,但是看到网上一些崩溃信息是xxx was deallocated while key value observers were still registered with it,感觉还是需要提防一下的)

重要函数

-[FBKVOController observe:keyPath:options:block:]

先从-[FBKVOController observe:keyPath:options: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;
  }

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

  [self _observe:object info:info];
}

可见它是初始化一个_FBKVOInfo并且调用了-[FBKVOController _observe:info:]方法,这个_FBKVOInfo保存的是监听的信息和回调的block,它的成员变量如下:

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

可已看出它不仅包含了_block这个属性,还包含了_action_context,如果调用了-[FBKVOController observe:keyPaths:options:action:]或者-[FBKVOController observe:keyPaths:options:context:],就会给这两个属性赋值。它还有_state这个属性,这个主要用于标记当前的监听状态,这个放后面讲。

-[FBKVOController _observe:info:]

接下来看一下-[FBKVOController _observe:info:]这个方法:

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

这里主要是对_objectInfosMap这个对象进行操作,这个操作是加锁的,_objectInfosMap的初始化如下:

    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];

其中retainObserved就是是否持有的意思,也就是区分KVOController还是KVOControllerNonRetaining,在KVOControllerNonRetaining的场景下,_objectInfosMap的键是弱引用。

在从上面的代码可以看出_objectInfosMap的键是object(被观察者),值是一个NSMutableSet,里面存放的是_FBKVOInfo的集合。先看看_FBKVOInfo的hash函数:

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

这意味着集合中_FBKVOInfo_keyPath都不一样,在info被添加到集合之前,会先判断这个info_keyPath是否已经在集合中,否则不会添加到这个集合也不会执行下面真正的observe操作。

也就是说,如果重复监听,后面的监听不会生效。这就确保一个对象不会被重复添加成同一个_keyPath的观察者。

-[_FBKVOSharedController observe:info:]

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return;
  }
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

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

_FBKVOSharedController是个单例,在这个方法里面进行系统的kvo操作,在操作之前,先将info添加至_infos里,_infos是个NSMapTable,它的初始化如下:

_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];

可以看出,这个maptable是不持有对象的。

此外,NSPointerFunctionsObjectPointerPersonality这个opthon标识存放的对象通过指针地址去重,而不是hash值,这是因为_FBKVOInfo的重写了hash函数,hash为_keypath,而所有的观察操作都是交给_FBKVOSharedController(可能存在不同的被观察者有同一个_keypath或者不同的观察者观察同一个_keypath),所以直接通过指针地址去重即可。

后面用到了_state这个属性,它是一个_FBKVOInfoState的枚举,枚举值如下:

typedef NS_ENUM(uint8_t, _FBKVOInfoState) {
  _FBKVOInfoStateInitial = 0,
  _FBKVOInfoStateObserving,
  _FBKVOInfoStateNotObserving,
};

可以看出是标识info当前是初始化,正在观察,和不被观察的。再看这个函数最后的执行代码:

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

对于第一个条件判断挺好理解,主要是为了把_state置为_FBKVOInfoStateObserving,因为这个时候info基本都是先创建的。

对于第二个条件,主要是为了处理观察的选项带有NSKeyValueObservingOptionInitial的情况,先看一下它的描述:

If specified, a notification should be sent to the observer immediately, before the observer registration method even returns.

总体来说就是在addObserver之后,如果选项有NSKeyValueObservingOptionInitial,就会立刻回调给观察者,这个时候如果观察者在这个回调中取消观察,那样的话_state就会变成_FBKVOInfoStateNotObserving,这个场景下就会进到这个条件。然后先看下-[_FBKVOSharedController unobserve:info:]函数,它的实现如下:

-[_FBKVOSharedController unobserve:info:]

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

这个方法只会在_state_FBKVOInfoStateObserving的时候调用removeObserver这个系统API,如果是上面那种极端的场景下是不会调用的。所以要在第二个条件里面将观察者移除。

而在通常情况下,添加观察者会将info_state置为_FBKVOInfoStateNotObserving,而在移除观察者的时候会进行这个判断,同时会将infoinfos中移除。

这个_FBKVOSharedController主要作用就是负责观察对象,因为这是个单例,不会被释放掉,所以我们无需担心在多线程环境下观察者被释放的野指针问题。此外,它不会作添加和移除观察者的去重操作,这个工作交给各自的KVOController来做。上面讲述的-[FBKVOController _observe:info:]是添加观察者的去重,在移除观察者也是一样的,代码如下:

-[FBKVOController _unobserve: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];
}

如果registeredInfo,就是infos里没有对应的keypath时(重复移除的场景下),进到-[_FBKVOSharedController unobserve:info:]这个方法会直接return掉。

-[FBKVOController dealloc]

FBKVOController一般会跟随NSObject一起销毁,到时候dealloc就会被调用,它在dealloc进行如下操作:

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

因此,移除观察者的代码在大部分情况下都不需要开发者主动去调用。

-[_FBKVOSharedController observeValueForKeyPath:ofObject:change:context:]

最后就是被观察者属性变化时的处理了:

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

  {
    pthread_mutex_lock(&_mutex);
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);
  }

  if (nil != info) {

    FBKVOController *controller = info->_controller;
    if (nil != controller) {
      id observer = controller.observer;
      if (nil != observer) {

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

不难理解,其实就是根据context拿到info,这个info里面包含了所有需要的信息,通过这些信息决定怎么派发事件。

总结

这是一个强大KVO三方库,不仅使用方便,还规避了很多坑,另外,代码上也有很多值得借鉴的地方。

参考文献

iOS KVO的优势及缺点

iOS KVO崩溃全情景列举+解决方案分析

上一篇下一篇

猜你喜欢

热点阅读