iOS攔截系統KVO監聽,防止多次刪除和添加!!!Demo

https://blog.csdn.net/jq2530469200/article/details/52484646


最近項目中處理kvo 的時候,遇到一個問題:當我操作的時候,會發現kvo 釋放的時候,會崩潰, 崩潰日誌如下:

/*Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <SecondViewController 0x7f83d8f30a50> for the key path "kvoState" from <AppDelegate 0x7f83d8c067b0> because it is not registered as an observer.'*/

經過反覆研究,發現了錯誤的原因,並且找到解決錯誤的辦法

下面我將介紹一下我的思路:(慢慢來 跟着我的思路走)

1.我在AppDelegate裏面添加一個屬性

@property(nonatomic,copy)NSString *kvoState;/* 測試kvo設置的一個字段 */

2.我在我創建的一個ViewController(SecondViewController)裏面去監聽這個屬性

- (void)monitorNet

{

    AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;

    // kvo監聽屬性值的改變

    [appDelegate addObserver:self forKeyPath:@"kvoState" options:NSKeyValueObservingOptionNew context:nil];


}


/**

 *  kvo

 */

- (void)observeValueForKeyPath:(NSString *)keyPath          // 監聽的屬性名稱

                      ofObject:(id)object                   // 被監聽的對象

                        change:(NSDictionary *)change       // 屬性的值

                       context:(void *)context              // 添加監聽時傳來的值

{

    AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;

    if ([keyPath isEqualToString:@"kvoState"]) {             

        NSNumber *number = [change objectForKey:@"new"];

        NSInteger item = [number integerValue];

        NSLog(@"%@====",appDelegate.kvoState);              

        NSLog(@"%@----",number);

        if ([object isKindOfClass:[AppDelegate class]] ) {

            

        }

    }

    

}

然後我再去釋放 複寫系統 dealloc 這個方法

-(void)dealloc

{

    AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;

    [appDelegate removeObserver:self forKeyPath:@"kvoState"];

}

3.在第二步之後,我點擊一個button ,push 到 另外一個ViewController(

TestViewController)裏面,然後在TestViewController裏面,點擊button ,在這個button 的點擊事件裏面去執行下面的代碼:(特地演示錯誤)

-(void)buttonAction{

    SecondViewController *secondVC = [[SecondViewController alloc]init];/*執行此行代碼回報上述的錯誤*/

    [self.navigationController popViewControllerAnimated:YES];

}


當這個方法執行完之後,就會出現前面所展示的錯誤
/*Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <SecondViewController 0x7f83d8f30a50> for the key path "kvoState" from <AppDelegate 0x7f83d8c067b0> because it is not registered as an observer.'*/

爲什麼會出現這種錯誤呢????

其實出現這種錯誤也很簡單的:首先在buttonAction 這個方法內,secondVC 他是一個局部變量,現在是ARC 管理,當這個方法執行完成以後,會銷燬 secondVC 這個對象,那麼,很自然的就會調用 SecondViewController 裏面的 dealloc 這個方法

-(void)dealloc

{

    AppDelegate *appDelegate = (AppDelegate *)[UIApplicationsharedApplication].delegate;

    [appDelegate removeObserver:selfforKeyPath:@"kvoState"];

}

appDelegate 的屬性kvoState 會被remove,但是的這個時候,it is not registered as an observer所有,就會重新上述的崩潰現象


說了這麼多,大家能理解這個崩潰的原因了嗎?(PS:不懂的話也請繼續瞭解下面的內容)

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


既然問題的出現,那麼,肯定會伴隨着事務的解決,下面我講給大家講解幾個解決的方法(百度查資料的,親自驗證,安全可靠),方案有三種:

/**

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

     *  方案一 :利用 @try @catch

     *  方案二 :利用 模型數組 進行存儲記錄

     *  方案二 :利用 observationInfo 裏私有屬性

     *

     */


《方案一》

/**

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

 *  利用 @try @catc

 不得不說這種方法真是很Low,不過很簡單就可以實現。(對於初學者來說,如果不怕麻煩,確實可以使用這種方法)

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

     dealloc 方法裏面執行下面代碼(我只是舉個例子,監聽的對象不一樣,具體代碼也不一樣)

-(void)dealloc

{

    //方案一 :利用 @try @catch(只能針對刪除多次KVO的情況下)(解決方法1

AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;

@try {

[appDelegate removeObserver:self forKeyPath:@"kvoState"];

}

@catch (NSException *exception) {

  NSLog(@"多次刪除kvo 報錯了");

}

}

有個簡單的方法:給NSObject 增加一個分類,然後利用Run time 交換系統的 removeObserver方法,在裏面添加 @try @catch

    步驟:創建一個類目NSObject+DSKVO,執行代碼裏面的步驟

         然後可以在dealloc 方法裏面執行下面代碼(我只是舉個例子,監聽的對象不一樣,具體代碼也不一樣)

         AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;

         [appDelegate removeObserver:self forKeyPath:@"kvoState"];

那麼,那個類目裏面的代碼是這樣的:(導入頭文件:#import <objc/runtime.h>解決方法2

+ (void)load

{

    [self switchMethod];

}


+ (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);

}

#pragma mark - 第一種方案,利用@try @catch

// 交換後的方法

- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath

{

    @try {//相對應解決方法1而已,只是把@try @catch 寫在這裏而已

        [self removeDasen:observer forKeyPath:keyPath];

    } @catch (NSException *exception) {}


}

// 交換後的方法

- (void)addDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context

{

    [self addDasen:observer forKeyPath:keyPath options:options context:context];


}


這種方法 利用Run time交換系統的 removeObserver方法,在裏面添加 @try @catch
相對上述那種解決方法來說,理解稍微難那麼一點,但是,不需要移除kvo 的時候每次調用@try @catch(這樣省了很多代碼)
《方案二》

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

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

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

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

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

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

下面我講介紹代碼:

+ (void)load

{

    [self switchMethod];

}


+ (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);

}

上述兩個方法的代碼同案例1 的一樣(同樣是新建一個類目NSObject+DSKVO),然後在寫下面方法

#pragma mark - 第二種方案,利用私有屬性

// 交換後的方法

- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath

{

    NSMutableArray *Observers = [DSObserver sharedDSObserver];

    ObserverData *userPathData = [self observerKeyPath:keyPath];

    // 如果有該key值那麼進行刪除

    if (userPathData) {

        [Observers removeObject:userPathData];

        @try {//如果沒有寫@try @catch 的話,在 dealloc 中,那個被監聽的對象(appdelegate)必須要全局變量

            [self removeDasen:observer forKeyPath:keyPath];

        }

        @catch (NSException *exception) {

            

        }

        

    }

    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;

}

這種情況還需要新建幾個文件:DSObserver 、ObserverData
——————————————————————————————————————————————————————————————

#import <Foundation/Foundation.h>


@interface ObserverData : NSObject

@property (nonatomicstrong)id objc;

@property (nonatomiccopy)  NSString *keyPath;

- (instancetype)initWithObjc:(id)objc key:(NSString *)key;

@end



#import "ObserverData.h"


@implementation ObserverData

- (instancetype)initWithObjc:(id)objc key:(NSString *)key

{

    if (self = [super init]) {

        self.objc = objc;

        self.keyPath = key;

    }

    return self;

}

@end


---------------------------------------

#import <Foundation/Foundation.h>


@interface DSObserver : NSMutableArray


+ (instancetype)sharedDSObserver;

@end




#import "DSObserver.h"


@implementation DSObserver

+ (instancetype)sharedDSObserver

{

    static id objc;

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        objc = [NSMutableArray array];

    });

    return objc;

}


@end

上述就是方案二了

《方案三》
利用 observationInfo 裏私有屬性

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

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了,有知道原因的可以給我說下。

+ (void)load

{

    [self switchMethod];

}


+ (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);

}



#pragma mark - 第三種方案,利用私有屬性

// 交換後的方法

- (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;

}


上述就是這個問題的解決方法 
參考文章:點擊打開鏈接
參考人員:tyh
github地址點擊打開鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章