KVOController实现的一些思考和疑惑

head first设计模式的第二个设计模式就是观察者模式,非常常见的一个设计思想,很多语言都内置了它的实现。iOS也不例外,键值修改后会通知观察者(KVO)。不过那又臭又长的写法让人很难提起兴趣用它。所以就有了KVOController

前言

这篇文章只是我看完KVOController源码的一些疑惑,就是KVOController为什么要这样做。
系统KVO的实现可以看这篇KVO进阶 —— 源码实现探究
KVOController源码的分析可以看这篇优雅地使用 KVO

用NSMapTable代替NSMutableDictionary

NSMapTable和NSHashTable在很多第三方框架或者底层源码里都有用到。类似于更常用的NSDictionary,NSSet。
从他们的初始化方法中就能知道,他们最大的优点就是可以弱引用添加的对象,其实性能还是略差的。

//KVOController默认是强引用
- (instancetype)initWithObserver:(nullable id)observer
{
  return [self initWithObserver:observer retainObserved:YES];
}

//NSObject+FBKVOController.h
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

@property (nullable, nonatomic, weak, readonly) id observer;observer是weak引用,是为了避免循环依赖。

而KVOControllerNonRetaining是指被观察的object会被weak指针指向(NSMapTable的key就是object)。
这种情况我觉得是鸡肋,因为如果用KVOControllerNonRetaining来做,而object引用计数为0回收了,那么程序必崩溃,必须要调用(void)unobserve:(nullable id)object
而如果是KVOController,不需要观察了,也是调用- (void)unobserve:(nullable id)object引用计数会减1。
既然都需要调用unobserve,为什么还要提供KVOControllerNonRetaining呢?增加了崩溃的隐患,又没有实际作用。

NSMapTable除了有个参数是设置weak,strong,还有有个参数是NSPointerFunctionsObjectPointerPersonality。

NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
  • NSHashTableObjectPointerPersonality

使用移位指针(shifted pointer)来做hash检测及确定两个对象是否相等。我觉得就是根据指针的唯一性来比较的,应该就是==

这样object就不会调用isEqual,hash来判断是否相等。防止object的hash方法被重写导致错误。像kvo这种情况,就应该用==来判断是否是同一个object。

而_FBKVOInfo重写了isEqual,hash方法是因为NSMapTable里的NSMutableSet要区分是否同一个_FBKVOInfo。

所以这才是用NSMapTable的根本意义,就是需要NSHashTableObjectPointerPersonality。KVOControllerNonRetaining完全可以去掉。object就应该用strong指针指向,当不需要观察或者需要释放时调用remove,防止潜在的崩溃。

用_FBKVOSharedController单例来对属性进行监听

这个就是我看完源码最大的疑惑,为什么要用_FBKVOSharedController单例来监听。

要解开这个疑惑,我觉得还是要自己来实现一下。
首先不用考虑weak指向object,理由上面已经说过了。所以可以在_FBKVOInfo里加上object

@implementation _FBKVOInfo {
@public
    __weak LYKVOController *_controller;
    id _object;
    NSString *_keyPath;
    NSKeyValueObservingOptions _options;
    SEL _action;
    void *_context;
    LYKVONotificationBlock _block;
    _FBKVOInfoState _state;
}

存储的数据结构就用NSHashTable就可以了。

{
    NSHashTable<_FBKVOInfo *> *_infos;
    pthread_mutex_t _lock;
}  

具体的实现和一些判断就省略了,流程就是这样。不需要_FBKVOSharedController

- (void)_observe:(id)object info:(_FBKVOInfo *)info {
    [_infos addObject:object];
    [object addObserver:];
}

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context
{ 
    //...context里所有的信息都有
}

- (void)_unobserveAll {
    [_infos removeAllObjects];
}  

上面都没有什么问题,就是删除指定的object(path)的时候如果object非常多性能会很差。

- (void)unobserve:(nullable id) object {
    if (nil == object) {
        return;
    }
    
    pthread_mutex_lock(&_lock);
    
    NSMutableSet *deleteInfos = [[NSMutableSet alloc] init];
    for (_FBKVOInfo *info in _infos) {
        if (info->_state == _FBKVOInfoStateObserving && info->_object == object) {
            [info->_object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
            info->_state = _FBKVOInfoStateNotObserving;
            [deleteInfos addObject:info];
        }
    }
    
    for (_FBKVOInfo *info in deleteInfos) {
        [_infos removeObject:info];
    }
    
    // unlock
    pthread_mutex_unlock(&_lock);
}  

因为没有按object区分,所以删除的时候,必须要遍历一遍,这可能就是采用NSMapTable的原因。使得remove指定object的性能增加。
而使用了NSMapTable<id, NSMutableSet <_FBKVOInfo *> *_objectInfosMap就又有了新的问题。

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context
{
    pthread_mutex_lock(&_lock);
    
    _FBKVOInfo *info = (__bridge _FBKVOInfo *)context;
    NSMutableSet *infos = [_objectInfosMap objectForKey:object];
    _FBKVOInfo *existingInfo = [infos member:info];
    
    pthread_mutex_unlock(&_lock);

    ....
}

- (void)_unobserveAll
{
    pthread_mutex_lock(&_lock);
    
    for (NSMutableSet * set in _objectInfosMap) {
        for (_FBKVOInfo *info in set) {
            if (info->_state == _FBKVOInfoStateObserving) {
                [info->_object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
            }
            info->_state = _FBKVOInfoStateNotObserving;
        }
    }
    [_objectInfosMap removeAllObjects];
    
    pthread_mutex_unlock(&_lock);
}

- (void)unobserve:(nullable id) object {
    if (nil == object) {
        return;
    }
    
    pthread_mutex_lock(&_lock);
    
    NSMutableSet *infos = [_objectInfosMap objectForKey:object];
    
    for (_FBKVOInfo *info in infos) {
        if (info->_state == _FBKVOInfoStateObserving && info->_object == object) {
            [info->_object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
            info->_state = _FBKVOInfoStateNotObserving;
        }
    }
    
    [_objectInfosMap removeObjectForKey:object];
    // unlock
    pthread_mutex_unlock(&_lock);
}

会增加不少重复代码,而且每个函数的长度都很长,代码糅合在一块,很不优雅。
所以这才是增加_FBKVOSharedController单例来单独做observe和unobserve的目的。

更优雅的实现

//NSObject+FBKVOController.h

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;

- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath;

- (void)unobserve:(nullable id)object;

NSObject+FBKVOController.h应该直接给出方法,这样调用的时候就直接[self observe:keyPath:options:block:]

作者:levi
comments powered by Disqus