Crash攔截器 - KVO崩潰破除(再也不用擔心KVO讓你崩潰)

在本文中,我們將了解到如下內(nèi)容:

  1. 明晰KVO中的觀察者和被觀察者
  2. KVO導致崩潰的情況一覽
  3. 破除KVO崩潰的方案

前言

KVO(Key Value Observing) 也就是鍵值對觀察,它是iOS中觀察者模式的一種實現(xiàn)。KVO方便了我們做很多事情,但是在提供方便的時候,同樣給我們帶來了麻煩-最最最煩人的崩潰問題。
本文,我們將討論KVO導致崩潰的各種情況,以及給出解決這些崩潰問題的方案。

觀察者和被觀察者

在進行具體內(nèi)容的討論之前,我們先對觀察者被觀察者這兩個在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必須是在被觀察者中的有效路徑。
被觀察者的被觀察的屬性發(fā)生變化時,將會由觀察者中的observeValueForKeyPath:ofObject:change:context:方法進行響應。

KVO導致崩潰的情況一覽

我們現(xiàn)在先來把KVO導致崩潰的原因挨個擼出來,然后再想辦法解決掉所有的這些問題(思路清晰,沒毛病。我們解決所有問題的思路都應該是這樣的)。
下面我們先列舉出我們了解到的所有引起崩潰的原因:

  1. 添加或移除觀察時,keypath長度為0。
  2. 觀察者忘記寫監(jiān)聽回調(diào)方法observeValueForKeyPath:ofObject:change:context:。
  3. 添加和移除觀察的次數(shù)不匹配
    • 觀察者dealloc后沒有移除監(jiān)聽。
    • 移除未添加監(jiān)聽的觀察者。
    • 多次添加和移除觀察者,但添加和移除的次數(shù)不相同。
  4. 觀察者和被觀察者生命周期不一致,其中一個被釋放,而另一個未被釋放(比如兩個局部變量之間添加觀察)
    • 被觀察者被提前釋放,iOS10及以前會崩潰(筆者未能復現(xiàn))。
    • 觀察者提前被釋放,如果未移除觀察,則會崩潰。

PS:對于上面列舉到的各種情況,筆者在這里說明一下。觀察者dealloc后沒有移除監(jiān)聽* 這一情況應該是在iOS9中就被修復了,但是我找不到書面證據(jù)(略顯尷尬)。被觀察者被提前釋放,iOS10及以前會崩潰 這一情況我沒有弄出來,所以不是很確定其導致崩潰的原因,本文中將不會對其進行討論。*

破除KVO崩潰的思路

我們的目標是解決掉上述所有問題,并且要保證無侵入性
基于這樣的目的,我們有如下的思路:

  1. NSObject的分類中,使用Method Swizzling攔截addObserver:forKeyPath:options:context:removeObserver:forKeyPath:方法。removeObserver:forKeyPath:context:會在判斷context是否一致之后,再調(diào)用removeObserver:forKeyPath:移除監(jiān)聽。所以我們不置換removeObserver:forKeyPath:context:方法。
  2. 我們創(chuàng)建一個KVOProxy作為中間者,目的是使用KVOProxy代替對象完成所有的觀察和分發(fā)通知的功能。
  3. 觀察者添加觀察時,我們使KVOProxy作為真正的觀察者去添加對被觀察者的觀察,當被觀察者的屬性值有變化時,KVOProxy接收observeValueForKeyPath:ofObject:change:context:,然后再根據(jù)keypathofObject兩個參數(shù)去找到并通知觀察者
  4. 觀察者移除觀察時,我們在KVOProxy找到需要移除的觀察,再對觀察進行移除。

整體思路如上,這樣述說給我們的感覺很模糊,我們還是回我們最熟悉的方式:看代碼。

破除KVO崩潰的實現(xiàn)

首先我們先定義我們會使用到的類,中間者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有一些方法,我們先不做解釋,后續(xù)使用到時,我們再進行詳細討論。

NSObject分類會添加類型為KVOProxy的成員變量kvoProxy,代碼如下:

@interface NSObject (KVOProxy)

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

@end

我們再定義NSObject使用的用于保存通知相關信息的對象KVOProxyItem,代碼如下:

@interface KVOProxyItem : NSObject

@property (nonatomic, weak) id observed; // 弱引用被觀察者,防止循環(huán)引用
@property (nonatomic, weak) id observer; // 弱引用觀察者,如果觀察者被釋放,這里將會變?yōu)閚il
@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:如果被調(diào)起,我們可以判斷是否需要發(fā)送通知到觀察者

KVOProxy的私有屬性定義如下:

@interface KVOProxy ()

@property (nonatomic, assign) pthread_mutex_t mutex;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableSet<KVOProxyItem *> *> *proxyItemMap;

@end

互斥鎖mutex,考慮到可能會在多線程中添加或移除通知,所以我們需要做一些同步操作。
數(shù)據(jù)結構proxyItemMap,該字典中的keyKVOkeyPath。字典中的valueKVOProxyItem組成的集合,該集合保存了觀察者kvoProxy是當前KVOProxyItem對象的所有KVO對應的KVOProxyItem對象。
上面這段話讀起來可能比較繞口(表達能力就在這里,大家擔待點~~),我再對這段話做進一步的解釋:
我們會在addObserver:forKeyPath:options:context:時,創(chuàng)建一個KVOProxyItem對象:

 // 創(chuàng)建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];

這行代碼執(zhí)行之后,會在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是否一致之后,再調(diào)用removeObserver:forKeyPath:移除監(jiān)聽
 */
- (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;
    }
    
    // 創(chuàng)建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,則直接返回,避免出現(xiàn)添加觀察時,keypath長度為0的問題。
  3. 創(chuàng)建KVOProxyItem對象。
  4. 觀察者kvoProxy中添加KVOProxyItem對象,在添加成功的回調(diào)中,執(zhí)行真正的添加觀察的操作。但是此時的觀察者已經(jīng)換成了kvoProxy。
  5. kvo_removeObserver:forKeyPath:中根據(jù)keyPath被觀察者觀察者kvoProxy中移除對應的KVOProxyItem對象。在移除成功的回調(diào)中執(zhí)行真正的移除操作。

在步驟4中,我們可以避免多次添加觀察。

在步驟5中,我們可以避免移除不存在的觀察。

細心的我們肯定看到了這樣的代碼:

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

這是因為我們在添加KVO時,如果keyPath是多級,那么系統(tǒng)會自動拆分成多級進行監(jiān)聽。我們打印了整個過程,得到如下數(shù)據(jù):

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

keyPath逐級變化,而系統(tǒng)添加的后續(xù)步驟的觀察者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<NSKeyValueChangeKey, id> *)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;
    }
    
    // 判斷當前觀察者是否實現(xiàn)了方法observeValueForKeyPath:ofObject:change:context:
    // 這個地方用respondsToSelector:檢測,沒有用(未實現(xiàn),也返回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;
    }
    
    // 發(fā)送事件
    [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];
    
    // 必須解鎖之后再進行回調(diào),否則會導致啟動后屏幕不顯示內(nèi)容
    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已經(jīng)被釋放掉,導致判斷出錯
        // 但是判斷出錯對邏輯不影響,因為如果要向observed發(fā)送變更通知,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<KVOProxyItem *> * _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));
}

/// 根據(jù)指定的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不合法的監(jiān)聽進行過濾。并且如果發(fā)現(xiàn)有keyPath、observer、observed的監(jiān)聽,則認為是重復添加,我們則不再添加新的監(jiān)聽。
  2. proxy_removeObserved:keyPath:didRemoveBlock:中。
    • 我們對keyPathobserver、observed進行匹配。如果發(fā)現(xiàn)有一致的,則移除監(jiān)聽。如果沒有,則不做移除操作。從而避免過多移除監(jiān)聽而造成的崩潰。
  3. dealloc中,我們移除self的所有監(jiān)聽,防止出現(xiàn)對象被釋放,但是未移除監(jiān)聽的問題。
  4. observeValueForKeyPath:ofObject:change:context:中。
    • 我們keyPath、observerobserved進行匹配,只有在匹配到之后,才會進行通知的分發(fā)。
      此時,當observerobservednil時,是無法進行分發(fā)的,從而避免了observerobserved被填釋放導致崩潰的問題。
    • 在真正分發(fā)之前,我們需要判斷observer是否實現(xiàn)了方法observeValueForKeyPath:ofObject:change:context:,如果未實現(xiàn),則不進行分發(fā)。從而避免其引起的崩潰問題。

以上,就是關于KVO崩潰破除的所有解釋了,文章寫得可能不夠清晰。但是筆者已經(jīng)很努力了,如果不夠好,只能請大家諒解了(手動臉紅)。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯(lián)系作者。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容