Crash拦截器 - KVO崩溃破除(再也不用担心KVO让你崩溃)

在本文中,我们将了解到如下内容:

  1. 明晰KVO中的观察者和被观察者
  2. KVO导致崩溃的情况一览
  3. 破除KVO崩溃的方案

前言

KVO(Key Value Observing) 也就是键值对观察,它是iOS中观察者模式的一种实现。KVO方便了我们做很多事情,但是在提供方便的时候,同样给我们带来了麻烦-最最最烦人的崩溃问题。
本文,我们将讨论KVO导致崩溃的各种情况,以及给出解决这些崩溃问题的方案。

观察者和被观察者

在进行具体内容的讨论之前,我们先对观察者被观察者这两个在KVO中的角色进行明晰。
我们看如下代码:

 1. [self addObserver:self.myView forKeyPath:@"myLabel.text" options:NSKeyValueObservingOptionNew context:nil];
 2. [self.myView.myLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];

对于代码 1 而言,其中观察者self.myView被观察者self
对于代码 2 而言,其中观察者self被观察者self.myView.myLabel
简单来讲,在addObserver左边的是被观察者,在右边的是观察者
keyPath必须是在被观察者中的有效路径。
被观察者的被观察的属性发生变化时,将会由观察者中的observeValueForKeyPath:ofObject:change:context:方法进行响应。

KVO导致崩溃的情况一览

我们现在先来把KVO导致崩溃的原因挨个撸出来,然后再想办法解决掉所有的这些问题(思路清晰,没毛病。我们解决所有问题的思路都应该是这样的)。
下面我们先列举出我们了解到的所有引起崩溃的原因:

  1. 添加或移除观察时,keypath长度为0。
  2. 观察者忘记写监听回调方法observeValueForKeyPath:ofObject:change:context:
  3. 添加和移除观察的次数不匹配
    • 观察者dealloc后没有移除监听。
    • 移除未添加监听的观察者。
    • 多次添加和移除观察者,但添加和移除的次数不相同。
  4. 观察者和被观察者生命周期不一致,其中一个被释放,而另一个未被释放(比如两个局部变量之间添加观察)
    • 被观察者被提前释放,iOS10及以前会崩溃(笔者未能复现)。
    • 观察者提前被释放,如果未移除观察,则会崩溃。

PS:对于上面列举到的各种情况,笔者在这里说明一下。观察者dealloc后没有移除监听* 这一情况应该是在iOS9中就被修复了,但是我找不到书面证据(略显尴尬)。被观察者被提前释放,iOS10及以前会崩溃 这一情况我没有弄出来,所以不是很确定其导致崩溃的原因,本文中将不会对其进行讨论。*

破除KVO崩溃的思路

我们的目标是解决掉上述所有问题,并且要保证无侵入性
基于这样的目的,我们有如下的思路:

  1. NSObject的分类中,使用Method Swizzling拦截addObserver:forKeyPath:options:context:removeObserver:forKeyPath:方法。removeObserver:forKeyPath:context:会在判断context是否一致之后,再调用removeObserver:forKeyPath:移除监听。所以我们不置换removeObserver:forKeyPath:context:方法。
  2. 我们创建一个KVOProxy作为中间者,目的是使用KVOProxy代替对象完成所有的观察和分发通知的功能。
  3. 观察者添加观察时,我们使KVOProxy作为真正的观察者去添加对被观察者的观察,当被观察者的属性值有变化时,KVOProxy接收observeValueForKeyPath:ofObject:change:context:,然后再根据keypathofObject两个参数去找到并通知观察者
  4. 观察者移除观察时,我们在KVOProxy找到需要移除的观察,再对观察进行移除。

整体思路如上,这样述说给我们的感觉很模糊,我们还是回我们最熟悉的方式:看代码。

破除KVO崩溃的实现

首先我们先定义我们会使用到的类,中间者KVOProxy

@interface KVOProxy : NSObject

- (void)proxy_addObserverWithProxyItem:(KVOProxyItem *)proxyItem didAddBlock:(dispatch_block_t)didAddBlock;
- (void)proxy_removeObserved:(NSObject *)observed keyPath:(NSString *)keyPath didRemoveBlock:(dispatch_block_t)didRemoveBlock;
- (void)proxy_removeAllObserver;

@end

KVOProxy有一些方法,我们先不做解释,后续使用到时,我们再进行详细讨论。

NSObject分类会添加类型为KVOProxy的成员变量kvoProxy,代码如下:

@interface NSObject (KVOProxy)

@property (nonatomic, readonly, strong) KVOProxy *kvoProxy;

@end

我们再定义NSObject使用的用于保存通知相关信息的对象KVOProxyItem,代码如下:

@interface KVOProxyItem : NSObject

@property (nonatomic, weak) id observed; // 弱引用被观察者,防止循环引用
@property (nonatomic, weak) id observer; // 弱引用观察者,如果观察者被释放,这里将会变为nil
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) NSKeyValueObservingOptions options;
@property (nonatomic, assign) void *context;

- (instancetype)initWithObserver:(id)observer observed:(id)observed keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

@end

我们在KVOProxyItem保存了通知相关的所有属性,其中观察者(observer)和被观察者(observed)都是使用的弱引用。当观察者被观察者被释放后,observeValueForKeyPath:ofObject:change:context:如果被调起,我们可以判断是否需要发送通知到观察者

KVOProxy的私有属性定义如下:

@interface KVOProxy ()

@property (nonatomic, assign) pthread_mutex_t mutex;
@property (nonatomic, strong) NSMutableDictionary *> *proxyItemMap;

@end

互斥锁mutex,考虑到可能会在多线程中添加或移除通知,所以我们需要做一些同步操作。
数据结构proxyItemMap,该字典中的keyKVOkeyPath。字典中的valueKVOProxyItem组成的集合,该集合保存了观察者kvoProxy是当前KVOProxyItem对象的所有KVO对应的KVOProxyItem对象。
上面这段话读起来可能比较绕口(表达能力就在这里,大家担待点~~),我再对这段话做进一步的解释:
我们会在addObserver:forKeyPath:options:context:时,创建一个KVOProxyItem对象:

 // 创建KVOProxyItem
 KVOProxyItem *item = [[KVOProxyItem alloc] initWithObserver:observer observed:self keyPath:keyPath options:options context:context];

那么这个item就是这次KVO对应的KVOProxyItem对象。

我们再以下面这行代码为例:

[self addObserver:self.myView forKeyPath:@"myLabel.text" options:NSKeyValueObservingOptionNew context:nil];

这行代码执行之后,会在self.myViewkvoProxyproxyItemMap中以@"myLabel.text"key的集合中,添加一个这次KVO对应的KVOProxyItem对象。

上面说了这么多,我们还是先把相关的代码贴出来,大家一起过过眼吧。

NSObject的代码如下:

@implementation NSObject (KVOProxy)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self kvo_swizzleSelector:@selector(addObserver:forKeyPath:options:context:)
                       toSelector:@selector(kvo_addObserver:forKeyPath:options:context:)];
        [self kvo_swizzleSelector:@selector(removeObserver:forKeyPath:)
                       toSelector:@selector(kvo_removeObserver:forKeyPath:)];
    });
}

/*
 removeObserver:forKeyPath:context: 会在判断context是否一致之后,再调用removeObserver:forKeyPath:移除监听
 */
- (void)kvo_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    if (keyPath.length <= 0) {
        NSLog(@"keyPath is empty for observer:%@...", observer);
        return;
    }
    
    if ([observer isKindOfClass:NSClassFromString(@"NSKeyValueObservance")]) {
        [self kvo_addObserver:observer forKeyPath:keyPath options:options context:context];
        return;
    }
    
    // 创建KVOProxyItem
    KVOProxyItem *item = [[KVOProxyItem alloc] initWithObserver:observer observed:self keyPath:keyPath options:options context:context];
    
    // 向观察者的kvoProxy添加KVOProxyItem,如果成功则在self作为被观察者添加观察者observer.kvoProxy
    [observer.kvoProxy proxy_addObserverWithProxyItem:item didAddBlock:^{
        [self kvo_addObserver:observer.kvoProxy forKeyPath:keyPath options:options context:context];
    }];
}

- (void)kvo_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    [observer.kvoProxy proxy_removeObserved:self keyPath:keyPath didRemoveBlock:^{
        [self kvo_removeObserver:self.kvoProxy forKeyPath:keyPath];
    }];
}

- (KVOProxy *)kvoProxy {
    id proxy = objc_getAssociatedObject(self, _cmd);
    if (proxy == nil) {
        proxy = [KVOProxy new];
        objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return proxy;
}

@end
  1. 首先,在load中进行方法交换。
  2. kvo_addObserver:forKeyPath:options:context:中进行判断,如果keyPath.length <= 0,则直接返回,避免出现添加观察时,keypath长度为0的问题。
  3. 创建KVOProxyItem对象。
  4. 观察者kvoProxy中添加KVOProxyItem对象,在添加成功的回调中,执行真正的添加观察的操作。但是此时的观察者已经换成了kvoProxy
  5. kvo_removeObserver:forKeyPath:中根据keyPath被观察者观察者kvoProxy中移除对应的KVOProxyItem对象。在移除成功的回调中执行真正的移除操作。

在步骤4中,我们可以避免多次添加观察。

在步骤5中,我们可以避免移除不存在的观察。

细心的我们肯定看到了这样的代码:

    if ([observer isKindOfClass:NSClassFromString(@"NSKeyValueObservance")]) {
        [self kvo_addObserver:observer forKeyPath:keyPath options:options context:context];
        return;
    }

这是因为我们在添加KVO时,如果keyPath是多级,那么系统会自动拆分成多级进行监听。我们打印了整个过程,得到如下数据:

observer:MyView - keyPath:myView.myLabel.text
observer:NSKeyValueObservance - keyPath:myLabel.text
observer:NSKeyValueObservance - keyPath:text

keyPath逐级变化,而系统添加的后续步骤的观察者NSKeyValueObservance对象,所以我们需要进行一次过滤。

KVOProxy的代码如下:

@implementation KVOProxy

- (void)dealloc {
    // 被释放前移除所有观察
    [self proxy_removeAllObserver];
    pthread_mutex_destroy(&(_mutex));
}

- (instancetype)init {
    self = [super init];
    if (self) {
        pthread_mutex_init(&(_mutex), NULL);
        self.proxyItemMap = @{}.mutableCopy;
    }
    return self;
}

#pragma mark - KVO Handle
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (keyPath.length <= 0 || object == nil) {
        return;
    }
    
    [self lock];
    
    __block KVOProxyItem *item = nil;
    NSSet *set = self.proxyItemMap[keyPath];
    [set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
        if (object == obj.observed &&
            self == [obj.observer kvoProxy]) {
            *stop = YES;
            item = obj;
        }
    }];
    
    [self unlock];
    
    if (item == nil) {
        return;
    }
    
    // 如果观察者被提前释放,则打印错误信息
    if (item.observer == nil) {
        NSLog(@"observer is nil when %@ observe keyPath:%@", [item.observed class], item.keyPath);
        return;
    }
    
    // 判断当前观察者是否实现了方法observeValueForKeyPath:ofObject:change:context:
    // 这个地方用respondsToSelector:检测,没有用(未实现,也返回YES)
    SEL selector = @selector(observeValueForKeyPath:ofObject:change:context:);
    
    BOOL exist = NO;
    unsigned int count = 0;
    Method *methoList = class_copyMethodList([item.observer class], &count);
    for (int i = 0; i < count; i++) {
        Method method = methoList[i];
        if (method_getName(method) == selector) {
            exist = YES;
            break;
        }
    }
    
    if (!exist) {
        /*
         An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
         */
        NSLog(@"observer:%@ can not respond observeValueForKeyPath:ofObject:change:context:", item.observer);
        return;
    }
    
    // 发送事件
    [item.observer observeValueForKeyPath:keyPath ofObject:object change:change context:item.context];
}

#pragma mark - Public Methods
- (void)proxy_addObserverWithProxyItem:(KVOProxyItem *)proxyItem didAddBlock:(dispatch_block_t)didAddBlock {
    if (proxyItem == nil) {
        return;
    }
    
    if (proxyItem.keyPath.length <= 0) {
        NSLog(@"keyPath is empty for observer:%@...", proxyItem.observer);
        return;
    }
    
    [self lock];
    
    __block BOOL added = NO;
    NSMutableSet *set = self.proxyItemMap[proxyItem.keyPath];
    [set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
        if (obj.observer == proxyItem.observer &&
            obj.observed == proxyItem.observed) {
            *stop = YES;
            added = YES;
        }
    }];
    
    if (added) {
        NSLog(@"observer:%@ for keyPath:%@ is added", [proxyItem.observer class], proxyItem.keyPath);
        [self unlock];
        return;
    }
    
    if (set == nil) {
        set = [NSMutableSet set];
        [self.proxyItemMap setObject:set forKey:proxyItem.keyPath];
    }
    
    [set addObject:proxyItem];
    
    [self unlock];
    
    // 必须解锁之后再进行回调,否则会导致启动后屏幕不显示内容
    didAddBlock();
}

- (void)proxy_removeObserved:(NSObject *)observed keyPath:(NSString *)keyPath didRemoveBlock:(dispatch_block_t)didRemoveBlock {
    if (observed == nil || keyPath.length <= 0) {
        return;
    }
    
    [self lock];
    
    NSMutableSet *set = self.proxyItemMap[keyPath];
    __block KVOProxyItem *item = nil;
    [set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
        // 这里可能因为observed已经被释放掉,导致判断出错
        // 但是判断出错对逻辑不影响,因为如果要向observed发送变更通知,observed必须不为nil
        if (observed == obj.observed) {
            item = obj;
            *stop = YES;
        }
    }];
    
    if (item) {
        [set removeObject:item];
        didRemoveBlock();
    }
    
    [self unlock];
}

- (void)proxy_removeAllObserver {
    [self.proxyItemMap enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableSet * _Nonnull obj, BOOL * _Nonnull stop) {
        [obj enumerateObjectsUsingBlock:^(KVOProxyItem * _Nonnull obj, BOOL * _Nonnull stop) {
            [obj.observed removeObserver:self forKeyPath:obj.keyPath];
        }];
    }];
}

#pragma mark - Private Methods
- (void)lock {
    pthread_mutex_lock(&(_mutex));
}

- (void)unlock {
    pthread_mutex_unlock(&(_mutex));
}

/// 根据指定的keyPath和observed在proxyItemMap查找KVOProxyItem
- (KVOProxyItem *)proxyItemForKeyPath:(NSString *)keyPath observed:(id)observed {
    NSMutableSet *set = self.proxyItemMap[keyPath];
    __block KVOProxyItem *item = nil;
    [set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
        if (observed == obj.observed) {
            item = obj;
            *stop = YES;
        }
    }];
    return item;
}

@end
  1. proxy_addObserverWithProxyItem:didAddBlock:中。
    • 我们对keyPath不合法的监听进行过滤。并且如果发现有keyPathobserverobserved的监听,则认为是重复添加,我们则不再添加新的监听。
  2. proxy_removeObserved:keyPath:didRemoveBlock:中。
    • 我们对keyPathobserverobserved进行匹配。如果发现有一致的,则移除监听。如果没有,则不做移除操作。从而避免过多移除监听而造成的崩溃。
  3. dealloc中,我们移除self的所有监听,防止出现对象被释放,但是未移除监听的问题。
  4. observeValueForKeyPath:ofObject:change:context:中。
    • 我们keyPathobserverobserved进行匹配,只有在匹配到之后,才会进行通知的分发。
      此时,当observerobservednil时,是无法进行分发的,从而避免了observerobserved被填释放导致崩溃的问题。
    • 在真正分发之前,我们需要判断observer是否实现了方法observeValueForKeyPath:ofObject:change:context:,如果未实现,则不进行分发。从而避免其引起的崩溃问题。

以上,就是关于KVO崩溃破除的所有解释了,文章写得可能不够清晰。但是笔者已经很努力了,如果不够好,只能请大家谅解了(手动脸红)。

你可能感兴趣的:(Crash拦截器 - KVO崩溃破除(再也不用担心KVO让你崩溃))