[iOS] KVOController源码分析

消失的四分之三月小姐姐复习了一下之前写的好几十篇文章,发现写时一时爽,写后火葬场。终于开始恢复更新啦~

KVOController

Git:https://github.com/facebook/KVOController

KVOController builds on Cocoa's time-tested key-value observing implementation. It offers a simple, modern API, that is also thread safe. Benefits include:

  • Notification using blocks, custom actions, or NSKeyValueObserving callback.
  • No exceptions on observer removal.
  • Implicit observer removal on controller dealloc.
  • Thread-safety with special guards against observer resurrection

1. 获取一个KVOController实例

我们可以直接从自己拿到一个KVOController,因为KVOController给NSObject加了一个category:

#import "NSObject+FBKVOController.h"

#import 

#if !__has_feature(objc_arc)
#error This file must be compiled with ARC. Convert your project to ARC or specify the -fobjc-arc flag.
#endif

#pragma mark NSObject Category -

NS_ASSUME_NONNULL_BEGIN

static void *NSObjectKVOControllerKey = &NSObjectKVOControllerKey;
static void *NSObjectKVOControllerNonRetainingKey = &NSObjectKVOControllerNonRetainingKey;

@implementation NSObject (FBKVOController)

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

@end

可以看到其实就是创建了一个FBKVOController然后作为关联对象保存给了self。注意这里可以拿到两种controller,一个是KVOController,另一种是KVOControllerNonRetaining,他们的区别就是在alloc init的时候传入的retainObserved是YES还是NO。


2. FBKVOController初始化

上面的retainObserved到底是啥呢我们现在来看看~

@implementation FBKVOController
{
  NSMapTable *> *_objectInfosMap;
  pthread_mutex_t _lock;
}

#pragma mark Lifecycle -

+ (instancetype)controllerWithObserver:(nullable id)observer
{
  return [[self alloc] initWithObserver:observer];
}

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

- (instancetype)initWithObserver:(nullable id)observer
{
  return [self initWithObserver:observer retainObserved:YES];
}

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

每个FBKVOController对象会有两个成员变量:一个是_lock防止对table的多线程操作;一个是NSMapTable记录了被监听对象以及监听了他的哪些属性的infos。

我们默认初始化方法initWithObserver其实最后也是调到了initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved,并且默认是retain的~

在初始化的时候,先init了_objectInfosMap,如果retainObserved为YES,那么_objectInfosMap的key就是NSPointerFunctionsStrongMemory的,反之则是NSPointerFunctionsWeakMemory,也就是map不持有key的强引用。然后init了一下lock。

然后下面正好看到了dealloc,里面也很简单就是调用了自己的unobserveAll方法,以及destroy了锁~


3. 如何addObserver

我们使用的observe方法是做了什么呢?

- (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,其实就是一个model类用于记录keyPath之类的:

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

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

然后调用了内部的_observe

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

之前我们创建的_lock其实就是在这里用的~ 感觉所有lock几乎都是给set map之类的操作用的0.0

上面这段其实就是把我们刚才创建的_FBKVOInfo存起来。_objectInfosMap这个map是以object为key的,然后value是一个set,存了这个target所有加的KVOInfo,如果这个set里面已经包含了要加的info就会直接return,如果没有就加到set里然后调用[[_FBKVOSharedController sharedController] observe:object info:info]; }

- (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有一个hashtable用于记录infos,是weak memory的哈,这里先把info加进去,然后给object真正的add KVO啦~~ 最后改一下state~~

这里比较神奇的是为啥会有info->_state == _FBKVOInfoStateNotObserving的情况,看注释其实就是如果是initial的那种KVO,在add的时候就会回调了,也就是[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info]这一行,那么如果在回调里面又remove了KVO,到下面的时候这个state就已经是_FBKVOInfoStateNotObserving了~

需要注意的是add KVO的时候info做为context哦

我们终于看完了怎么加的,然后来看下如何回调的~ 因为上面加监听的时候observer是self,也就是_FBKVOSharedController,所以会回调下面的:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary *)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 *changeWithKeyPath = change;
          // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
          if (keyPath) {
            NSMutableDictionary *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];
        }
      }
    }
  }
}

这里其实就是回调observeValueForKeyPath的时候可以直接从里面拿context,然后把context转为info,得到kvocontroller以及info->_block,为了防止监听了同一个object的多个keyPath,它在回调的时候往change字典里面塞了一个FBKVONotificationKeyPathKey为key的path。

到这里就完整的进行了改变属性触发回调block了~ 下面来看下remove以及自动remove~

[iOS] KVOController源码分析_第1张图片
监听实现

4. removeObserver

我们可以手动调用removeObserver:

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

  // unobserve object property
  [self _unobserve:object info:info];
}

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

  [self _unobserve:object];
}

- (void)unobserveAll
{
  [self _unobserveAll];
}

如果传入了keyPath,那么会创建一个_FBKVOInfo哈~ 也可以只取消某个某个被观察者的所有KVO,还可以取消这个KVOController的所有观察,这里每个NSObject会有自己的KVOController哈

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

  // get observation infos
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // lookup registered info instance
  _FBKVOInfo *registeredInfo = [infos member:info];

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

    // remove no longer used infos
    if (0 == infos.count) {
      [_objectInfosMap removeObjectForKey:object];
    }
  }

  // unlock
  pthread_mutex_unlock(&_lock);

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

我们移除某个特定观察者的监听keyPath的时候,它是从_objectInfosMap里面先找到这个被监听这的info set,然后找到我们要取消的set拿出去,也就是删除掉记录,最后调用[[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo]

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

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

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

_FBKVOSharedController也是把info先从记录移除,然后removeObserver并改一下info->_state。这里为了防止已经被移除的仍旧执行remove会判断一下state~

移除某个被观察者的所有监听之诶的其实就是循环遍历infos,然后执行上面的移除~


我们上面其实很多地方都在判断这个info是不是已经添加了,之所以可以这么做,是因为info有覆写等同性:

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

也就是对于info而言只要监听的keyPath是同一个,就可以被认为是相等的哈~~


  • 此外为什么_FBKVOSharedController还要有个infos集合呢?明明kvocontroller已经用map+set的方式记录了被观察者们的info了吖。

这里其实是因为KVO被触发的时候回调的是_FBKVOSharedController的方法,然后这个时候传入的context就是info其实,但是作者从infos里面找了一下是不是在infos里面,如果不是就return了相当于。

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

我猜之所以不能拿着context转为info直接用,大概因为infos是weak memory的,如果这个info已经由于某些原因被回收了,那么这个时候我们就在infos里面找不到了,虽然我觉得这个猜测不是很合理叭,如果有知道为啥不能拿着context直接用的朋友欢迎探讨啊~

另外一个问题是,为啥info重写了isEqual以及hash改为了看keyPath的情况下,如果监听了两个不同的对象的相同keyPath属性,所创建的两个info都可以加入到infos的hashTable呢,明明他俩是equal的啊?

这个原因是,其实这里的infos的等同性判断是由NSPointerFunctionsObjectPointerPersonality定义的,也就是不仅仅要hash和isEqual一样,pointer地址的shift的hash也要一样。

  • 我理解的之所以info要重写等同性,是为了在FBKVOController- (void)_observe:(id)object info:(_FBKVOInfo *)info里面可以获取重复加入的对相同object监听的相同keyPath的existingInfo,防止重复的keyPath监听。但这个_FBKVOSharedController单例里面的infos是公用的,并且不想让它keep住info的指针就用了weak的HashTable,但对于不同的被监听object即使是相同keyPath的info也应该可以加入,所以等同性就增加了NSPointerFunctionsObjectPersonality来判断pointer也得偏移hash一样才不能重复加入。

※ 自动移除监听

KVOController的一个好处就是我们可以不用必须手动unobserve,在对象dealloc的时候会自动移除所有监听:

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

也就是其实是依赖于KVOController被回收的时候去unObserveAll的,而KVOController其实是NSObject的一个fake属性,存在了它的关联dict里。

我们init KVOController的时候有一个属性是要不要保留被观察者的强指针,如果是需要的话也就是retain的方式,就会用strong key的map来保存被观察者;如果是non retain的方式,就会用weak key的map来保存被观察者。

先考虑一下retain的方式会有什么问题,KVOController会持有_objectInfosMap_objectInfosMap强持有了被观察的对象,如果我们是用self监听self,那么这个时候self就会持有一个KVOControllerKVOController又持有了self,这个时候self是无法被释放掉的,那么KVOController也自然不能被释放。这种情况就需要我们在对象即将销毁的时候手动的unobserveAll或者取消对这个对象的所有观察

现在考虑一下non-retain的KVOController,还是上面那个case的话,_objectInfosMapweak持有了被观察的对象,这个时候即使self观察了self,也不会被KVOController持有,那么self是可以释放的,然后就可以释放KVOController了,KVOController在释放走dealloc的时候就会自动的unobserveAll了。

我之前有写关于non-retain的KVOController可能有点问题,如果dealloc的时候被观察的object已经是nil了,unobserveAll的时候就不能把之前添加的observer移除,那么在observer监听对象销毁以后,再次触发了监听对象的KVO事件,会导致程序崩溃

但上面这个我并没有复现,好像是其实observer监听者是被unsafe指针引用的,如果不removeObserver也是可以正常释放的(这点是肯定的),但是如果你再改它监听的东西,触发KVO回调就会crash。所以比较妥的方式还是要removeObserver的

虽然iOS9及以上notification已经可以不用手动remove就会自动移除,但之前的应该也是类似的思路会unsafe keep观察者,如果再次触发通知会crash叭。

anyway手动移除也好,自动移除也好,不要内存泄漏以及crash就好~


5. 等同性option

hashtable判断是不是同一个object有很多可选的option,默认的的那种是判断use -hash and -isEqual, object description是不是一致,其他的可以看注释哈,感觉还是英文说得更清楚~

// Personalities are mutually exclusive
// default is object.  As a special case, 'strong' memory used for Objects will do retain/release under non-GC
NSPointerFunctionsObjectPersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (0UL << 8),         // use -hash and -isEqual, object description
NSPointerFunctionsOpaquePersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (1UL << 8),         // use shifted pointer hash and direct equality
NSPointerFunctionsObjectPointerPersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (2UL << 8),  // use shifted pointer hash and direct equality, object description
NSPointerFunctionsCStringPersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (3UL << 8),        // use a string hash and strcmp, description assumes UTF-8 contents; recommended for UTF-8 (or ASCII, which is a subset) only cstrings
NSPointerFunctionsStructPersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (4UL << 8),         // use a memory hash and memcmp (using size function you must set)
NSPointerFunctionsIntegerPersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (5UL << 8),        // use unshifted value as hash & equality

我试了一下default的是hash和isEqual决定的,好像木有和description相关哈~
如果加了NSPointerFunctionsOpaquePersonality就即使hash和isEqual相同也会重复加入的,因为指针不同


KVOController用单例作为观察者都回调到一个对象上就比较舒服,你就不用覆写或者hock了,只要记录了回调回来以后要执行神马就OK啦~ 这个库真的是比较精炼的~~ 有时间可以康康吖~~ (P.S.鸣谢周六晚上被我问问题到12点的小哥哥,我真的不是故意的。。

Reference:
https://www.jianshu.com/p/1f7d70ff2002

你可能感兴趣的:([iOS] KVOController源码分析)