iOS - FBKVOController 实现原理

本文导读:

1.系统KVO的问题
2.FBKVOController优点
3.FBKVOController的架构设计图
4.FBKVOController源码详读
5.FBKVOController总结

一.系统KVO的问题

  • 当观察者被销毁之前,需要手动移除观察者,否则会出现程序异常(向已经销毁的对象发送消息);
  • 可能会对同一个被监听的属性多次添加监听,这样我们会接收到多次监听的回调结果;
  • 当观察者对多个对象的不同属性进行监听,处理监听结果时,需要在监听回调的方法中,作出大量的if判断;
  • 当对同一个被监听的属性进行两次removeObserver时,会导致程序crash。这种情况通常出现在父类中有一个KVO,在父类的dealloc中remove一次,而在子类中再次remove。

二. FBKVOController优点

  • 可以同时对一个对象的多个属性进行监听,写法简洁;
  • 通知不会向已释放的观察者发送消息;
  • 增加了block和自定义操作对NSKeyValueObserving回调的处理支持;
  • 不需要在dealloc 方法中手动移除观察者,而且移除观察者不会抛出异常,当FBKVOController对象被释放时, 观察者被隐式移除;

三.FBKVOController架构设计图

FBKVOController_00.png

四.FBKVOController源码详解

FBKVOController源码详解分四部分:分别是对两个私有类_FBKVOInfo,_FBKVOSharedController和两个公开类FBKVOController,NSObject+FBKVOController的源码解读:

(一)FBKVOController

首先我们创建一个FBKVOController的实例对象时,有以下三种方法,一个类方法和两个对象方法,

//该方法是一个全能初始化的对象方法,其他初始化方法内部均调用该方法
//参数:observer是观察者,retainObserved:表示是否强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved 

//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer;

//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
+ (instancetype)controllerWithObserver:(nullable id)observer;
NS_DESIGNATED_INITIALIZER;

我们先来看全能初始化方法内部的实现,该方法对三个实例变量_observer(观察者),_objectInfosMap(NSMapTable,被监听对象->被监听属性集合之间的映射关系),pthread_mutex_init(互斥锁):

//全能初始化方法
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
      
    //观察者
    _observer = observer;

//NSMapTable中的key可以为对象,而且可以对其中的key和value弱引用
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
      
//对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER
//对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

这里请先思考以下问题:

  • 属性observer为何使用weak,它和哪个对象之间会导致循环引用问题,是如何导致循环引用问题的?
  • 为何不使用字典来保存被监听对象和被监听属性集合之间的关系?
  • NSDictionary的局限性有哪些?NSMapTable相对字典,有哪些优点?
  • 互斥锁是为了保证哪些数据的线程安全?

带着这些问题我们来看FBKVOController内部是如何实现监听的,这里我们只看带Block回调的一个监听方法,其他几个方法和这个方法内部实现是相同的。下面的方法内部做了如下工作:
1.传入的参数keyPath,block为空时,程序闪退,同时报出误提示;
2.对传入参数为空的判读;
3.利用传入的参数创建_FBKVOInfo对象;
4.调用内部私有方法实现注册监听;

//观察者监听object中健值路径(keyPath)所对应属性的变化
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
//NSAssert是一个预处理宏, 它可以让开发者比较便捷的捕获错误, 让程序闪退, 同时报出错误提示
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);

//首先判断被监听的对象是否为空,被监听的健值路径是否为空,回调的block是否为空
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }

  // 根据传进来的参数创建_FBKVOInfo对象,将这些参数封装到_FBKVOInfo对象中
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

  // 监听对象object的属性信息(_FBKVOInfo对象)
  [self _observe:object info:info];
}

该私有方法内部并没有实现真正的注册监听,这里使用NSMapTable保存了被监听对象object-> _FBKVOInfo对象集合的关系,具体的监听是在_FBKVOSharedController类中实现的。观察者可以监听多个对象,而每个对象中可能有多个属性被监听,其关系如下图:


FBKVOController_01.png

内部实现思路:

  • 对当前线程访问的数据_objectInfosMap进行加锁;
  • 根据被监听对象object到_objectInfosMap取出被监听的属性信息对象集合infos;
  • 判断被监听的属性对象info是否存在集合中;
  • 如果已经存在,则不需要再次添加监听,防止多次监听;
  • 如果获取的集合infos为空,则建存放_FBKVOInfo对象的集合infos,保存映射关系:object->infos;
  • 将被监听的信息_FBKVOInfo对象存到集合infos中;
  • 解锁,其他线程可以访问该数据;
  • 调用_FBKVOSharedController 的方法实现监听;
//该方法是内部私有方法
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  //先加锁,访问_objectInfosMap
  pthread_mutex_lock(&_lock);

    //到_objectInfosMap中根据key(被监听的对象)获取被监听的属性信息集合
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

   //判断infos集合中是否存在被监听属性信息对象info
  _FBKVOInfo *existingInfo = [infos member:info];

    //被监听对象的属性已经存在,不需要再次监听,防止多次添加监听
  if (nil != existingInfo) {
  
  //解锁,其他线程可以再次访问_objectInfosMap中的数据
    pthread_mutex_unlock(&_lock);
    return;
  }

  //根据被监听对象在_objectInfosMap获取的被监听属性信息的集合为空
  if (nil == infos) {
    //懒加载创建存放_FBKVOInfo对象的set集合infos
    infos = [NSMutableSet set];

    //保存被监听对象和被监听属性信息的映射关系object->infos
    [_objectInfosMap setObject:infos forKey:object];
  }

  // 将被监听的信息_FBKVOInfo对象存到集合infos中
  [infos addObject:info];

  //解锁
  pthread_mutex_unlock(&_lock);

   //最终的监听方法是通过_FBKVOSharedController中的方法来实现
  //_FBKVOSharedController内部实现系统KVO方法
  [[_FBKVOSharedController sharedController] observe:object info:info];
}
(二)_FBKVOInfo

_FBKVOInfo私有类的内部很简单,没有任何业务逻辑,只是一个简单的Model,主要是将以下的实例变量封装到对象中,方便访问:

{
@public
//weak,防止循环引用
  __weak FBKVOController *_controller;
   //被监听属性的健值路径
  NSString *_keyPath;

//NSKeyValueObservingOptionNew:观察修改前的值
// NSKeyValueObservingOptionOld:观察修改后的值
//NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
//NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(一次修改有两次触发)
  NSKeyValueObservingOptions _options;

//被监听属性值变化时的回调方法
  SEL _action;

//上下文信息(void * 任何类型)
  void *_context;
//被监听属性值变化时的回调block
  FBKVONotificationBlock _block;
//监听状态
  _FBKVOInfoState _state;
}

_FBKVOInfo私有类提供了一个全能初始化方法,来初始化以上实例变量。其他几个部分初始化方法内部均调用该全能初始化方法。

//全能初始化方法
- (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私有类还重写了isEqual:和hash方法,用来进行_FBKVOInfo对象的判等性。当我们在自定义对象时,需要重写isEqual:和hash方法,作为自定义对象相等性的判断。

  • 优化判断对象相等性的效率:
    1.首先判断hash值是否相等,若相等则进行第2步;若不等,则直接判断不等;hash值是对象判等的必要非充分条件;(即没它一定不行,有它不一定行)
    2.在hash值相等的情况下,再进行对象判等, 作为判等的结果;
    关于对象相等性判断,请看大神Mattt Thompson的一篇博客 Equality
//当重写hash方法时,我们可以将关键属性的hash值进行位或运算来作为hash值
- (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];
}

//输出对象的调试信息
//description: 使用NSLog从控制台输出对象的信息
 //debugDescription:通过断点po打印输出对象的信息
- (NSString *)debugDescription
{
  NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p keyPath:%@", NSStringFromClass([self class]), self, _keyPath];
  if (0 != _options) {
    [s appendFormat:@" options:%@", describe_options(_options)];
  }
  if (NULL != _action) {
    [s appendFormat:@" action:%@", NSStringFromSelector(_action)];
  }
  if (NULL != _context) {
    [s appendFormat:@" context:%p", _context];
  }
  if (NULL != _block) {
    [s appendFormat:@" block:%p", _block];
  }
  [s appendString:@">"];
  return s;
}
  • 请分析如果将实例变量__weak FBKVOController *_controller前的 __weak去掉,它和_FBKVOInfo对象之间的循环引用环是如何形成的?
(三)_FBKVOSharedController

_FBKVOSharedController私有类内部实现了系统KVO的方法,用来接收和转发KVO的通知。接口中提供了监听和移除监听的方法。其接口如下:

@interface _FBKVOSharedController : NSObject

// 单例初始化方法
+ (instancetype)sharedController;

// 监听object的属性
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info;

//移除对object中属性的监听
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info;

// 移除对object中多个属性的监听
- (void)unobserve:(id)object infos:(nullable NSSet *)infos;

@end

_FBKVOSharedController私有类内部有两个私有成员变量,_infos是用来存放_FBKVOInfo对象,_infos可以对其中的成员变量弱引用,这也是为何使用NSHashTable,而不使用NSSet来存放_FBKVOInfo对象的原因。_mutex是互斥锁:

{
    //存放被监听属性的信息对象
  NSHashTable<_FBKVOInfo *> *_infos;
    //互斥锁
  pthread_mutex_t _mutex;
}

_FBKVOSharedController私有类的初始化方法,支持iOS 系统和Mac系统,初始化实例变量_infos,指定了_infos对存放在其中的成员变量弱引用,及判等性方式:

//提供全局的单例初始化方法,该单例对象的生命周期与程序的生命周期相同
+ (instancetype)sharedController
{
  static _FBKVOSharedController *_controller = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _controller = [[_FBKVOSharedController alloc] init];
  });
  return _controller;
}
//初始化成员变量_infos和_mutex
- (instancetype)init
{
  self = [super init];
  if (nil != self) {
    //初始化实例变量
    NSHashTable *infos = [NSHashTable alloc];
      
   // iOS 系统下:hashTable中的对象是弱引用,对象的判等方式:位移指针的hash值和直接判等
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
    _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];

  //MAC系统下
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
    if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
      _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
    } else {
      // silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
      _infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
    }

#endif
      //初始化互斥锁
    pthread_mutex_init(&_mutex, NULL);
  }
  return self;
}

- (void)dealloc
{
    //对象被销毁时,销毁互斥锁
  pthread_mutex_destroy(&_mutex);
}

_FBKVOSharedController在这个方法中,调用系统KVO方法,将自己注册为观察者,思路如下:
1.首先将被监听的信息对象_FBKVOInfo保存到_infos中;
2.然后调用系统KVO方法将自己注册为被监听对象object的观察者;
3.最后修改监听的状态;当不再监听时,安全移除观察者;

//添加监听
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
    //被监听的属性信息_FBKVOInfo对象为空时,直接返回
  if (nil == info) {
    return;
  }

    // 加锁,防止多线程访问时,出现数据竞争
  pthread_mutex_lock(&_mutex);

   // 将被监听的属性信息info对象添加到_infos中,_infos对成员变量info是弱引用
  [_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) {
      
      //不再监听时安全移除观察者
    // 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];
  }
}

实现系统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;
  {
    pthread_mutex_lock(&_mutex);
  //确定_infos是否包含给定的对象context,若存在返回该对象,否则返回nil;
  //所使用的相等性比较取决于所选择的选项
  //例如,使用NSPointerFunctionsObjectPersonality选项将使用isEqual:方法来判断相等。
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);
  }

//通过上下文参数context传过来的被监听的_FBKVOInfo对象,已经存在_infos中
  if (nil != info) {
  
 //_FBKVOSharedController对象强引用FBKVOController对象,防止被提前释放
 //因为在_FBKVOInfo中,对FBKVOController对象是弱引用
    FBKVOController *controller = info->_controller;
    if (nil != controller) {

      //强引用观察者,在FBKVOController中,FBKVOController对象弱引用观察者observer,防止在使用时已经被释放
      id observer = controller.observer;
      if (nil != observer) {

        //使用自定义block回传监听结果
        if (info->_block) {

          NSDictionary<NSString *, id> *changeWithKeyPath = change;

          //将keyPath添加到字典中以便在观察多个keyPath时,能够清晰知道监听的是哪个keyPath
          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];
        }
      }
    }
  }
}

_FBKVOSharedController实现了移除观察者的方法,思路如下:
1.首先从_infos中移除被监听的属性信息对象info;
2.然后根据监听状态,通过调用系统的方法,移除正在被监听的属性信息对象info;
3.最后修改监听状态;

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

    //先从HashTable中移除被监听的属性信息对象
  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;
}
(四)NSObject+FBKVOController

NSObject+FBKVOController 分类比较简单,它主要通过runtime方法,以懒加载的形式给 NSObject ,创建并关联一个 FBKVOController 的对象。

@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end

五.FBKVOController总结

FBKVOController是线程安全的,相对于系统的KVO而言,使用起来更方便,安全,简洁。
1.NSHashTable和NSMapTable的使用;
2.互斥锁pthread_mutex_t的使用
3.FBKVOController和Observer之间循环引用的形成和解决;
4.FBKVOController和_FBKVOInfo之间循环引用的形成和解决;

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

推荐阅读更多精彩内容