FBKVOController源码解析

在平常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崩溃全情景列举+解决方案分析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345