痛快的使用KVO ---- FBKVOController源码分析

前言

KVO是iOS开发当中必不可少的一个工具,可以说是使用最广泛的工具之一。无论你是要在检测某一个属性变化,还是构建viewmodel双向绑定UI以及数据,KVO都是一个十分使用的工具。

然而!!

KVO用起来太TMD麻烦了,要注册成为某个对象属性的观察者,要在适当的时候移除观察者状态,还要写毁掉函数,更蛋疼的是对象属性还要用字符串作为表示。其中任何一个地方都要注意很多点,而且因为Delegate回调函数的原因,导致代码分离,可读性极差,维护起来异常费劲。

所以说,对于我来说,能不用的时候,尽量绕过去用其他的方法,直到我发现了Facebook的开源框架KVOController


基本介绍

1、主要结构

屏幕快照 2017-07-19 上午12.51.20.png

事实上KVOController的实现只有2各类,第一个是NSObject的Category是我们使用的类,第二个则是具体的实现方法。

2、NSObject + FBKVOController 分析

在Category的.h文件中有两个属性,根据备注可知区别在意一个是持有的,另一个不是。

/**
 @abstract Lazy-loaded FBKVOController for use with any object
 @return FBKVOController associated with this object, creating one if necessary
 @discussion This makes it convenient to simply create and forget a FBKVOController, and when this object gets dealloc'd, so will the associated     controller and the observation info.
 */
@property (nonatomic, strong) FBKVOController *KVOController;

/**
 @abstract Lazy-loaded FBKVOController for use with any object
 @return FBKVOController associated with this object, creating one if necessary
 @discussion This makes it convenient to simply create and forget a FBKVOController.
 Use this version when a strong reference between controller and observed object would create a retain cycle.
 When not retaining observed objects, special care must be taken to remove observation info prior to deallocation of the observed object.
 */
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

Category的.m文件和其他文件类似,写的都是setter以及getter方法,并且在getter方法中对别对两个属性做了对于 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);
}

3、FBKVOController分析

1)几个基本API

/**
 @abstract Creates and returns an initialized KVO controller instance.
 @param observer The object notified on key-value change.
 @return The initialized KVO controller instance.
 */
+ (instancetype)controllerWithObserver:(nullable id)observer;


/**
 @abstract Registers observer for key-value change notification.
 @param object The object to observe.
 @param keyPath The key path to observe.
 @param options The NSKeyValueObservingOptions to use for observation.
 @param block The block to execute on notification.
 @discussion On key-value change, the specified block is called. In order to avoid retain loops, the block must avoid referencing the KVO controller or an owner thereof. Observing an already observed object key path or nil results in no operation.
 */
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;


/**
 @abstract Registers observer for key-value change notification.
 @param object The object to observe.
 @param keyPath The key path to observe.
 @param options The NSKeyValueObservingOptions to use for observation.
 @param action The observer selector called on key-value change.
 @discussion On key-value change, the observer's action selector is called. The selector provided should take the form of -propertyDidChange, -    propertyDidChange: or -propertyDidChange:object:, where optional parameters delivered will be KVO change dictionary and object observed. Observing nil or observing an already observed object's key path results in no operation.
 */
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action;


/**
 @abstract Block called on key-value change notification.
 @param observer The observer of the change.
 @param object The object changed.
 @param change The change dictionary which also includes @c FBKVONotificationKeyPathKey
 */
typedef void (^FBKVONotificationBlock)(id _Nullable observer, id object, NSDictionary<NSKeyValueChangeKey, id> *change);
  • 第一个很简单了,是创建KVOController的实例
  • 第二个是注册键值变化的观察者,返回一个有固定参数的Block。需要注意的是,为了避免循环引用,尽量避免使用KVOController及其持有者。
  • 第三个和第二个一样,也是注册键值变化的观察者,但是返回的是一个选择子SEL,API介绍中还对选择子SEL进行了建议。
  • 第四个很简单,是第二个回调函数的Block。值得注意的是,observer以及object分别是变化的观察者以及属性变化的对象,所以我们书写的时候可以改成我们需要的样式,以此来免去另加的转换过程。

主要的实现逻辑

KVOController的实现需要有两个私有的成员变量:

  • NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
  • pthread_mutex_t _lock;

以及另一个暴露在外只读的属性:

  • @property (nullable, nonatomic, weak, readonly) id observer;

在实现过程中,作为 KVO 的管理者,其必须持有当前对象所有与 KVO 有关的信息,而在 KVOController 中,用于存储这个信息的数据结构就是 NSMapTable。为了保证线程安全,需要持有pthread_mutex_t锁,用于在操作NSMapTable时候使用。

1、下面让我们看初始化方法:

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

很简单,主要工作是持有了传进来的Observer,初始化了NSMapTable以及初始化了pthread_mutex_t锁。
值得一提的是初始化** NSMapTable,我们回看第二部分,在属性的区分就在于是否是持有,根据属性的名字也能看出,不持有的话,引用计数就不会加一。所以在初始化的时候明显的区分就是在创建NSPointerFunctionsOptions的时候,是StrongMemory还是WeakMemory。
通过方法
+ (instancetype)controllerWithObserver:(nullable id)observer**初始化的时候,默认为持有。

2、注册观察者

通常情况下我们会使用可以回调Block的API,但是也有少数情况下会选择传递选择子SEL的API,我们这里只拿传递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;
  }

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

  // observe object with info
  [self _observe:object info:info];
}

在这里传递进来的一些参数会被封装成为私有的_FBKVOInfo,那我们来简单看一下_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:(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;
}

由此可以看出,** _FBKVOInfo的主要作用就是起到了一个类似Model一样存储主要数据的作用,并储存了一个_FBKVOInfoState作为表示当前的 KVO 状态。
需要注意的是,成员变量都是用了
@public修饰。
另外,对
- (NSString )debugDescription以及- (NSString )debugDescription两个方法做了重写,方便了使用以及Debug。

之后执行了私有方法*- (void)_observe:(id)object info:(_FBKVOInfo )info

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

1)首先先进行的是对于自身持有的 _objectInfosMap这个成员变量的操作,一切都需要在先锁定,执行结束再解锁的过程。

  • 首先获取了对于当前观察者的注册的关注列表。
  • 判断是否当前需要关注的信息是否在此列表中,如果有则return出去,不再进行关注。
  • 如果当前的关注列表不存在则此时创建一个
  • 将关注的信息储存在关注列表中。

2)然后是获取了** _FBKVOSharedController单例并且执行了单例的- (void)observe:(id)object info:(nullable _FBKVOInfo )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];
  }
}

加锁,对于当前单例的NSHashTable进行添加操作的信息,并执行Foundation

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

然后对信息中的state进行更改。

3、观察并回调

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

这个就相对简单了,主要是根据关注信息内是Block还是Action来执行,如果两者都没有就会调用观察者 KVO 回调方法。

4、注销观察

事实上,注销是在执行dealloc的时候执行的,同时也去掉了锁:

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

因为KVO事件都由私有的** _KVOSharedController** 来处理,所以当每一个** KVOController ** 对象被释放时,都会将它自己持有的所有 KVO 的观察者交由** _KVOSharedControlle** r的方法处理,我们再来看下代码:

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

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

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

该方法会遍历所有传入的** _FBKVOInfo** ,从其中取出keyPath 并将 ** _KVOSharedController ** 移除观察者。

当然,假如你需要手动的移除某一个的观察者,** _KVOSharedController ** 也提供了方法:

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

总结

这套框架提供了丰富的结构,基本能够满足我们对于KVO的使用需求。
只需要一次代码,就可以完成对一个对象的键值观测,同时不需要处理移除观察者,也可以在同一处代码进行键值变化之后的处理,从恶心的回调方法中解脱出来,不仅提供了使用方便,也不需要我们手动主要观察者,避免了各种问题,绝对算的上一个完善好用的框架。


Refrence


另外

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

推荐阅读更多精彩内容

  • KVO 作为 iOS 中一种强大并且有效的机制,为 iOS 开发者们提供了很多的便利;我们可以使用 KVO 来检测...
    JzRo阅读 925评论 0 2
  • KVO 作为 iOS 中一种强大并且有效的机制,为 iOS 开发者们提供了很多的便利;我们可以使用 KVO 来检测...
    Draveness阅读 6,885评论 11 59
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,013评论 0 26
  • FBKVOController是一个简单易用的键值观察框架,KVOController 对于 Cocoa 中 KV...
    我有小尾巴快看阅读 1,382评论 0 0
  • 《没蕃故人》张籍前年伐月支,城上没全师。蕃汉断消息,死生长别离。无人收废帐,归马识残旗。欲祭疑君在,天涯哭此时。 ...
    秋凉柠檬阅读 821评论 0 1