FBKVOController实现原理

作者: Cooci_和谐学习_不急不躁 | 来源:发表于2019-03-05 21:08 被阅读294次
    • 1.系统KVO的问题
    • 2.FBKVOController优点
    • 3.FBKVOController的架构设计图
    • 4.FBKVOController源码详读
    • 5.FBKVOController总结

    转载:iOS - FBKVOController 实现原理

    一.系统KVO的问题

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

    二. FBKVOController优点

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

    三.FBKVOController架构设计图

    image

    四.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类中实现的。观察者可以监听多个对象,而每个对象中可能有多个属性被监听,其关系如下图:

    image

    内部实现思路:

    • 对当前线程访问的数据_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之间循环引用的形成和解决;

    转载:iOS - FBKVOController 实现原理

    相关文章

      网友评论

        本文标题:FBKVOController实现原理

        本文链接:https://www.haomeiwen.com/subject/hrkauqtx.html