我是靠谱客的博主 超级星星,最近开发中收集的这篇文章主要介绍62. KVOController详解,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

结构

Facebook出品的这个KVOController算上.h文件一共只有5个文件,如图:

其中NSObject+FBKVOController 之中只是制作了两个属性

@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
复制代码

这两个属性关联的FBKVOController都是通过懒加载创建的,关于这两个属性存放的数据差异,我们先放一下。

而在另一个文件FBKVOController中,则存在三个Class,分别为

@implementation _FBKVOInfo
{
@public
  // 简单的值保存
  __weak FBKVOController *_controller;
  
  // KVO所监听的属性
  NSString *_keyPath;
  // KVO的可选项
  NSKeyValueObservingOptions _options;
  
  // 下面试三种不同的处理方式 优先级为 Block > SEL > context
  // 这三种方式为互斥的,优先执行优先级高的
  SEL _action;
  void *_context;
  FBKVONotificationBlock _block;
  
  // 当前Info对应的KVO行为状态
  _FBKVOInfoState _state;
}

@implementation _FBKVOSharedController
{
  // 通过直接的地址比较而进行保存的一个NSHashTable 保存的是全部的KVO信息
  NSHashTable<_FBKVOInfo *> *_infos;
  // 访问上面的_infos的锁
  pthread_mutex_t _mutex;
}

@implementation FBKVOController
{
  // 使用hash和isEqual来进行校对的MapTable 
  // 其中保存
  // Key为监听的对象
  // Value的Set中保存的是KVO信息
  NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
  
  // 同理也是锁
  pthread_mutex_t _lock;
}
// 这个对象对应的那个回调响应者(俗称:监听者)
@property (nullable, nonatomic, weak, readonly) id observer;
复制代码

我们先从_FBKVOInfo开始

_FBKVOInfo

_FBKVOInfo 一共8个方法,其中5个是初始化方法,一个Debug文本,一个Hash一个equal判断。
初始化方法就不谈,关于Hash和equal有一篇文章说的很好点这里
总的来说:

Hash是在判断isEqual的必要非充分条件

按照官方的话来说

Returns an integer that can be used as a table address in a hash table structure.

If two objects are equal (as determined by the isEqual: method), they must have the same hash value. This last point is particularly important if you define hash in a subclass and intend to put instances of that subclass into a collection.

If a mutable object is added to a collection that uses hash values to determine the object’s position in the collection, the value returned by the hash method of the object must not change while the object is in the collection. Therefore, either the hash method must not rely on any of the object’s internal state information or you must make sure the object’s internal state information does not change while the object is in the collection. Thus, for example, a mutable dictionary can be put in a hash table but you must not change it while it is in there. (Note that it can be difficult to know whether or not a given object is in a collection.)


非官方译文

返回一个在HashTable结构中用作表地址的integer(没有符号的long)

如果你有两个对象是通过isEqual:来判断的相等,那么你必须要满足两者的hash值为相同。如果你在子类中定义了hash方法,并且把这个子类放到了一个容器集合中,那么这后面的一点对你来说尤为重要(两者hash要相同)。

如果一个可变对象在一个集合中使用hash值来进行定位的话,那么当对象在容器中,可千万不要让这个对象的hash方法返回结果出现改变。(因为hash是索引线索)

所以hash方法的返回结果不应该与任何的内部信息出现依赖,并且在这个对象在集合中的时候应该保证对象的内部信息不要出现改变。

比如:你在HashTable中存在一个可变的字典,那么当这个字典存在于HashTable中的时候,你就不要去改变他(这样做会让判断这个集合中是否存在这个指定对象变得很困难)(PS:经过不准确的测试,set、Dictionary等类型的Mut类型他们的hash值为count)


在这里要提一下在创建 option的描述文本的时候调用了一个这样的方法

__builtin_ctzl

/**
 返回最后一个1 然后修改传递进来的flags

 @param ptrFlags 整体的flags
 @return 获得的最后一个1
 */
static NSUInteger enumerate_flags(NSUInteger *ptrFlags)
{
  NSCAssert(ptrFlags, @"expected ptrFlags");
  if (!ptrFlags) {
    return 0;
  }

  NSUInteger flags = *ptrFlags;
  if (!flags) {
    // 如果全是0 表示解析完成了 返回0
    return 0;
  }

  // 这个相当于把最后一个1 拿到
  NSUInteger flag = 1 << __builtin_ctzl(flags);
  // 然后把这个东西从flags中删除掉
  flags &= ~flag;
  *ptrFlags = flags;
  return flag;
}
复制代码

这是处理二进制位的内置函数,关于更多的此类函数相关,可以通过这个链接进行查看 点这里


而关于其中的一个枚举类型,我们先不讲,留到后面再说。

typedef NS_ENUM(uint8_t, _FBKVOInfoState) {
  _FBKVOInfoStateInitial = 0,

  // whether the observer registration in Foundation has completed
  _FBKVOInfoStateObserving,

  // whether `unobserve` was called before observer registration in Foundation has completed
  // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions
  _FBKVOInfoStateNotObserving,
};
复制代码

其他的貌似没什么难点了 _(:з」∠)_

总体来说: _GFKVOInfo 是一个用以储存KVO信息的包括三种回调、观察的对象信息(包括KVO属性,观察路径等)同时还维护了当前对应KVO的状态信息。

FBKVOController

这个我想先把KVOController先讲一下,然后再回到KVOShareController。见谅

前面我们就能在KVOInfo中找到一个弱指针是这个类型,我们前面大概提了一下这个东西保存的KVO观察者信息的。老套路我们看下他的方法。(属性在前面已经梳理了一遍了啊)

一共18个方法,如下图:

大体分为了四类:

  1. 创建等生命周期
  2. debug信息
  3. 非公开类
  4. 公开类API

我们一类一类来看 首先生命周期中,我们在最开始NSObject那个拓展的时候,有说过存在两种不同的Controller,一种为普通,一种为Noretain。

在这里,就得说一下NSMapTable和NSMutableDictionary的区别了,在一定程度上这两者的感觉是一样的,都是键值存储。但是NSMapTable比NSMutableDictionary 在定制性上要比NSMutableDictionary要好。(PS:NSMapTable 就是可变类型)

但其实在储存性能上来说,NSMutableDictionary或者前面说的哈希表对于NSSet来说都是略微逊色的。所以在非特殊情况下还是以使用NSMutableSet和NSMutableDictionary为主。

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    _observer = observer;
    // retainObserved将会影响到Key值的保存特性
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    
    // Key值都是基于地址进行比较的 而对于Value则是使用hash和equal来进行比较的
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}
复制代码

之所以需要一个Weak的,是为了防止在NSObject中监听自身属性的时候出现循环引用,无法释放这一类情况。

再提一次注意在dealloc中释放锁

第二类我们直接跳过

第三类私有API 以及第四类 开放API 其实就是创建对应的KVOInfo然后进行Set的插入。

总体来说:
FBKVOController是通过一个观察者创建的,同时按照不同的需求可以选择对Key值进行retain类型保存还是weak类型保存。
在对象中维护了一张从监听目标到监听属性列表的映射表.
表中包括自身观察者创建的FBKVOController,以及各种KVO属性都存有(_FBKVOInfo)。

_FBKVOSharedController

这个对象是一个单例,虽然infos的创建写了一大堆其实就是创建一个弱类型保存的哈希表.(至于为什么用哈希表 前面讨论MapTable已经说过了)
在其中这个方法中

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

  // 这个哈希表存放的都是_FBKVOInfo
  // 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];

  // TOCHECKOUT:
  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];
  }
}
复制代码

init表示刚初始化出来,当然每一个创建的都是init状态,但是其中会有在GFKVOController中进行一个射靶,我想各位没有忘记在_GFKVOInfo中的hash和isEqual。
而如果通过射靶得到的还是init说明以前完全没有创建过这个info说明是一个全新的那么就修改他的状态,并添加监听。
但是还存在一种情况。
当在多种线程中操作这个KVO的监听和取消监听的时候,cancel在进行observer之前进行了,也就是说当info传入这个方法的时候info的state发生了变化,变为了_FBKVOInfoStateNotObserving,cacel的代码如下

- (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;
}
复制代码

当然因为没有发生init变Observing这个行为所以也就不会出现remove行为,但是在逻辑上这个KVO就不应该再存在了,所以我们通过NotObserving属性判断当前的KVO是错误添加。然后进行移除操作。

最后是KVO的系统回调,按照三种不同的回调方法按照优先级进行先后确认,拿到最高优先级的方法并进行回调。

最终梳理

_FBKVOInfo 作为信息承载体使用(Model)使用keypath的hash值进行校对。认定所有keypath相同的对象都相等。
FBKVOController 作为对一个特定观察者的观察对象管理类存在,自身保存一个Map用来做通过监听对象作为键值获取一个_FBKVOInfo的集合用以射靶,如果存在,获取保存在_FBKVOSharedController的哈希表中信息的备份。
_FBKVOSharedController 真正的KVO行为执行者,是KVO行为的派发者。自身保存了所有通过KVOController进行的所有的KVO行为,与众多FBKVOController中保存的_FBKVOInfo是对等的(PS:当出现没有添加KVO就被移除的可能就会出现比FBKVOController多的情况)
NSObject+FBKVOController 定义了两个不同种类的FBKVOController的属性,当对自己属性进行KVO的时候最好使用noreatin版本。

零碎

其实KVOController并不是我最近的主要关注点,一切也都是因缘际会吧。KVOController一直卡住我的点,就是两个宏定义。(我删掉了注释不然看起来实在太多)

#define FBKVOKeyPath(KEYPATH) 
@(((void)(NO && ((void)KEYPATH, NO)), 
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); 
NSCAssert(fbkvokeypath, @"Provided key path is invalid."); 
fbkvokeypath + 1; })))

#define FBKVOClassKeyPath(CLASS, KEYPATH) 
@(((void)(NO && ((void)((CLASS *)(nil)).KEYPATH, NO)), #KEYPATH))
复制代码

((void)(NO && ((void)KEYPATH, NO))
这一块第一个NO和第三个NO是为了让中间的KEYPATH不进行运算,这样就能防止,这里进行了getter方法而产生不必要的问题。相关文档在这里Compile-time Key Paths Verification

至于其他的

NS_ASSUME_NONNULL_BEGIN
// 这中间的所有参数默认为NONULL
NS_ASSUME_NONNULL_END

// 表明这个文件必须要在ARC环境下
#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
复制代码

后记

严格意义上这是我第一篇完成的博文吧,可能算不上太好,但总是个开始。
我也很庆幸我第一个完成的是KVOController,篇幅之类的也更加容易控制吧。
共勉。

相关文档

关于hash和Equal
二进制位内置函数
基础集合类
Compile-time Key Paths Verification

转载于:https://juejin.im/post/5a31e95ef265da432840de64

最后

以上就是超级星星为你收集整理的62. KVOController详解的全部内容,希望文章能够帮你解决62. KVOController详解所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(54)

评论列表共有 0 条评论

立即
投稿
返回
顶部