iOS開發-黑科技防止多次添加刪除KVO出現的問題

一、使用場景

有時候我們會忘記添加多次KVO監聽或者,不小心刪除如果KVO監聽,如果添加多次KVO監聽這個時候我們就會接受到多次監聽。
如果刪除多次kvo程序就會造成catch,如下圖


這時候我們就可以想一些方案來防止這種情況的發生。


二、使用技術

核心 : 利用runtime實現方法交換,進行攔截add和remove進行操作。

  1. 方案一 :利用 @try @catch
  2. 方案二 :利用 模型數組 進行存儲記錄
  3. 方案二 :利用 observationInfo 裏私有屬性

(1) 方案一(只能針對刪除多次KVO的情況下)
利用 @try @catc

不得不說這種方法真是很Low,但是很簡單就可以實現。
這種方法只能針對多次刪除KVO的處理,原理就是try catch可以捕獲異常,不讓程序catch。這樣就實現了防止多次刪除KVO。

@try {
        [self.btn removeObserver:self forKeyPath:@"kkl"];
    } 
@catch (NSException *exception) {
        NSLog(@"多次刪除了");
}

普通情況下,使用這種方法就需要每次removeObserver的時候,就加上去一個@try @catch
有個簡單的方法:給NSObject 增加一個分類,然後利用Run time 交換系統的 removeObserver方法,在裏面添加 @try @catch。

runtime 就不多說了,大家自己自己查下相關資料有很多。
下面就直接上實現代碼了:

NSObject+DSKVO.m

#import "NSObject+DSKVO.h"
#import <objc/runtime.h>
@implementation NSObject (DSKVO)

+ (void)load
{
    [self switchMethod];
}

// 交換後的方法
- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
    @try {
        [self removeDasen:observer forKeyPath:keyPath];
    } @catch (NSException *exception) {}
}

+ (void)switchMethod
{
    SEL removeSel = @selector(removeObserver:forKeyPath:);
    SEL myRemoveSel = @selector(removeDasen:forKeyPath:);

    Method systemRemoveMethod = class_getClassMethod([self class],removeSel);
    Method DasenRemoveMethod = class_getClassMethod([self class], myRemoveSel);

    method_exchangeImplementations(systemRemoveMethod, DasenRemoveMethod);
}

@end

(2) 方案二
利用 模型數組 進行存儲記錄

第一步 利用交換方法,攔截到需要的東西
1,是在監聽哪個對象。
2,是在監聽的keyPath是什麼。

第二步 存儲思路
1,我們需要一個模型用來存儲
哪個對象執行了addObserver、監聽的KeyPath是什麼。
2,我們需要一個數組來存儲這個模型。

第三步 進行存儲
1,利用runtime 攔截到對象和keyPath,創建模型然後進行賦值模型相應的屬性。
2,然後存儲進數組中去。

第三步 存儲之前的檢索處理
1,在存儲之前,爲了防止多次addObserver相同的屬性,這個時候我們就可以,遍歷數組,取出每個一個模型,然後取出模型中的對象,首先判斷對象是否一致,然後判斷keypath是否一致2,對於添加KVO監聽:如果不一致那麼就執行利用交換後方法執行addObserver方法。

3,對於刪除KVO監聽: 如果一致那麼我們就執行刪除監聽,否則不執行。

4,上代碼了:
NSObject+DSKVO.m

#import "NSObject+DSKVO.h"
#import "DSObserver.h"
#import "ObserverData.h"
#import <objc/runtime.h>
@implementation NSObject (DSKVO)

+ (void)load
{
    [self switchMethod];
}

// 交換後的方法
- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
    NSMutableArray *Observers = [DSObserver sharedDSObserver];
    ObserverData *userPathData = [self observerKeyPath:keyPath];
    // 如果有該key值那麼進行刪除
    if (userPathData) {
        [Observers removeObject:userPathData];
        [self removeDasen:observer forKeyPath:keyPath];
    }
 return;
}

// 交換後的方法
- (void)addDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    ObserverData *userPathData= [[ObserverData alloc]initWithObjc:self key:keyPath];
    NSMutableArray *Observers = [DSObserver sharedDSObserver];

    // 如果沒有註冊,那麼才進行註冊
    if (![self observerKeyPath:keyPath]) {
        [Observers addObject:userPathData];
        [self addDasen:observer forKeyPath:keyPath options:options context:context];
    }

}

// 進行檢索,判斷是否已經存儲了該Key值
- (ObserverData *)observerKeyPath:(NSString *)keyPath
{
    NSMutableArray *Observers = [DSObserver sharedDSObserver];
    for (ObserverData *data in Observers) {
        if ([data.objc isEqual:self] && [data.keyPath isEqualToString:keyPath]) {
                return data;
        }
    }
    return nil;
}

+ (void)switchMethod
{
    SEL removeSel = @selector(removeObserver:forKeyPath:);
    SEL myRemoveSel = @selector(removeDasen:forKeyPath:);
    SEL addSel = @selector(addObserver:forKeyPath:options:context:);
    SEL myaddSel = @selector(addDasen:forKeyPath:options:context:);

    Method systemRemoveMethod = class_getClassMethod([self class],removeSel);
    Method DasenRemoveMethod = class_getClassMethod([self class], myRemoveSel);
    Method systemAddMethod = class_getClassMethod([self class],addSel);
    Method DasenAddMethod = class_getClassMethod([self class], myaddSel);

    method_exchangeImplementations(systemRemoveMethod, DasenRemoveMethod);
    method_exchangeImplementations(systemAddMethod, DasenAddMethod);
}

ObserverData 模型類文件有兩個屬性

@property (nonatomic, strong)id objc;
@property (nonatomic, copy)  NSString *keyPath;

DSObserver 類是一個單例數組

@implementation DSObserver
+ (instancetype)sharedDSObserver
{
    static id objc;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        objc = [NSMutableArray array];
    });
    return objc;
}

@end

(3) 方案三
利用 observationInfo 裏私有屬性

第一步 簡單介紹下observationInfo屬性
1,只要是繼承與NSObject的對象都有observationInfo屬性.
2,observationInfo是系統通過分類給NSObject增加的屬性。
3,分類文件是NSKeyValueObserving.h這個文件
4,這個屬性中存儲有屬性的監聽者,通知者,還有監聽的keyPath,等等KVO相關的屬性。
5,observationInfo是一個void指針,指向一個包含所有觀察者的一個標識信息對象,信息包含了每個監聽的觀察者,註冊時設定的選項等。

@property (nullable) void *observationInfo;

6,observationInfo結構 (箭頭所指是我們等下需要用到的地方)



第二步 實現方案思路
1,通過私有屬性直接拿到當前對象所監聽的keyPath

2,判斷keyPath有或者無來實現防止多次重複添加和刪除KVO監聽。

3,通過Dump Foundation.framework 的頭文件,和直接xcode查看observationInfo的結構,發現有一個數組用來存儲NSKeyValueObservance對象,經過測試和調試,發現這個數組存儲的需要監聽的對象中,監聽了幾個屬性,如果監聽兩個,數組中就是2個對象。
比如這是監聽兩個屬性狀態下的數組


4,NSKeyValueObservance屬性簡單說明
_observer屬性:裏面放的是監聽屬性的通知這,也就是當屬性改變的時候讓哪個對象執行observeValueForKeyPath的對象。
_property 裏面的NSKeyValueProperty NSKeyValueProperty存儲的有keyPath,其他屬性我們用不到,暫時就不說了。


5,拿出keyPath
這時候思路就有了,首先拿出_observances數組,然後遍歷拿出裏面_property對象裏面的NSKeyValueProperty下的一個keyPath,然後進行判斷需要刪除或添加的keyPath是否一致,然後分別進行處理就行了。
補充:NSKeyValueProperty我當時測試直接kvc取出來的時候發現取不出來,報錯,後臺直接取keyPath就可以,然後就直接取keyPath了,有知道原因的可以給我說下。


6,上代碼

#import "NSObject+DSKVO.h"
#import <objc/runtime.h>
@implementation NSObject (DSKVO)

+ (void)load
{
    [self switchMethod];
}

// 交換後的方法
- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
   if ([self observerKeyPath:keyPath]) {
        [self removeDasen:observer forKeyPath:keyPath];
    }
}

// 交換後的方法
- (void)addDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    if (![self observerKeyPath:keyPath]) {
        [self addDasen:observer forKeyPath:keyPath options:options context:context];
    }
}

// 進行檢索獲取Key
- (BOOL)observerKeyPath:(NSString *)key
{
    id info = self.observationInfo;
    NSArray *array = [info valueForKey:@"_observances"];
    for (id objc in array) {
        id Properties = [objc valueForKeyPath:@"_property"];
        NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
        if ([key isEqualToString:keyPath]) {
            return YES;
        }
    }
    return NO;
}
+ (void)switchMethod
{
    SEL removeSel = @selector(removeObserver:forKeyPath:);
    SEL myRemoveSel = @selector(removeDasen:forKeyPath:);
    SEL addSel = @selector(addObserver:forKeyPath:options:context:);
    SEL myaddSel = @selector(addDasen:forKeyPath:options:context:);

    Method systemRemoveMethod = class_getClassMethod([self class],removeSel);
    Method DasenRemoveMethod = class_getClassMethod([self class], myRemoveSel);
    Method systemAddMethod = class_getClassMethod([self class],addSel);
    Method DasenAddMethod = class_getClassMethod([self class], myaddSel);

    method_exchangeImplementations(systemRemoveMethod, DasenRemoveMethod);
    method_exchangeImplementations(systemAddMethod, DasenAddMethod);
}

參考文章:http://www.bkjia.com/IOSjc/993206.html
參考人員:tyh
github地址:https://github.com/DaSens/DSKVO



文/品味_生活(簡書作者)
原文鏈接:http://js.sunansheng.com/p/6c6f3a24b1ef
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章