iOS開發(fā) - 黑科技防止多次添加刪除KVO出現(xiàn)的問題

------更新------:
之前沒有判斷observer是否一致亡呵,有個(gè)別情況會(huì)無法處理哥攘,所以更新添加了observer判斷

一、使用場景

有時(shí)候我們會(huì)忘記添加多次KVO監(jiān)聽或者诚些,不小心刪除如果KVO監(jiān)聽男韧,如果添加多次KVO監(jiān)聽這個(gè)時(shí)候我們就會(huì)接受到多次監(jiān)聽。
如果刪除多次kvo程序就會(huì)造成catch葛圃,如下圖


這時(shí)候我們就可以想一些方案來防止這種情況的發(fā)生千扔。


二、使用技術(shù)

核心 : 利用runtime實(shí)現(xiàn)方法交換装悲,進(jìn)行攔截add和remove進(jìn)行操作昏鹃。

  1. 方案一 :利用 @try @catch
  2. 方案二 :利用 模型數(shù)組 進(jìn)行存儲(chǔ)記錄
  3. 方案二 :利用 observationInfo 里私有屬性

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

不得不說這種方法真是很Low,但是很簡單就可以實(shí)現(xiàn)诀诊。
這種方法只能針對多次刪除KVO的處理洞渤,原理就是try catch可以捕獲異常,不讓程序catch属瓣。這樣就實(shí)現(xiàn)了防止多次刪除KVO载迄。

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

普通情況下,使用這種方法就需要每次removeObserver的時(shí)候抡蛙,就加上去一個(gè)@try @catch
有個(gè)簡單的方法:給NSObject 增加一個(gè)分類护昧,然后利用Run time 交換系統(tǒng)的 removeObserver方法,在里面添加 @try @catch粗截。

runtime 就不多說了惋耙,大家自己自己查下相關(guān)資料有很多。
下面就直接上實(shí)現(xiàn)代碼了:

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) 方案二
利用 模型數(shù)組 進(jìn)行存儲(chǔ)記錄

第一步 利用交換方法,攔截到需要的東西
1绽榛,是在監(jiān)聽哪個(gè)對象湿酸。
2,是在監(jiān)聽的keyPath是什么灭美。

第二步 存儲(chǔ)思路
1推溃,我們需要一個(gè)模型用來存儲(chǔ)
哪個(gè)對象執(zhí)行了addObserver、監(jiān)聽的KeyPath是什么届腐。
2铁坎,我們需要一個(gè)數(shù)組來存儲(chǔ)這個(gè)模型。

第三步 進(jìn)行存儲(chǔ)
1犁苏,利用runtime 攔截到對象和keyPath,創(chuàng)建模型然后進(jìn)行賦值模型相應(yīng)的屬性硬萍。
2,然后存儲(chǔ)進(jìn)數(shù)組中去傀顾。

第三步 存儲(chǔ)之前的檢索處理
1襟铭,在存儲(chǔ)之前,為了防止多次addObserver相同的屬性短曾,這個(gè)時(shí)候我們就可以寒砖,遍歷數(shù)組,取出每個(gè)一個(gè)模型嫉拐,然后取出模型中的對象哩都,首先判斷對象是否一致,然后判斷keypath是否一致2婉徘,對于添加KVO監(jiān)聽:如果不一致那么就執(zhí)行利用交換后方法執(zhí)行addObserver方法漠嵌。

3,對于刪除KVO監(jiān)聽: 如果一致那么我們就執(zhí)行刪除監(jiān)聽,否則不執(zhí)行盖呼。

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值那么進(jìn)行刪除
    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];

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

}

// 進(jìn)行檢索几晤,判斷是否已經(jīng)存儲(chǔ)了該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 模型類文件有兩個(gè)屬性

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

DSObserver 類是一個(gè)單例數(shù)組

@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是系統(tǒng)通過分類給NSObject增加的屬性蟹瘾。
3圾浅,分類文件是NSKeyValueObserving.h這個(gè)文件
4,這個(gè)屬性中存儲(chǔ)有屬性的監(jiān)聽者憾朴,通知者狸捕,還有監(jiān)聽的keyPath,等等KVO相關(guān)的屬性众雷。
5灸拍,observationInfo是一個(gè)void指針做祝,指向一個(gè)包含所有觀察者的一個(gè)標(biāo)識信息對象,信息包含了每個(gè)監(jiān)聽的觀察者,注冊時(shí)設(shè)定的選項(xiàng)等株搔。

@property (nullable) void *observationInfo;

6剖淀,observationInfo結(jié)構(gòu) (箭頭所指是我們等下需要用到的地方)


第二步 實(shí)現(xiàn)方案思路
1,通過私有屬性直接拿到當(dāng)前對象所監(jiān)聽的keyPath纤房,和observer

2,判斷keyPath是否有無翻诉,和observer是否對應(yīng)一直炮姨,來實(shí)現(xiàn)防止多次重復(fù)添加和刪除KVO監(jiān)聽。

3碰煌,通過Dump Foundation.framework 的頭文件舒岸,和直接xcode查看observationInfo的結(jié)構(gòu),發(fā)現(xiàn)有一個(gè)數(shù)組用來存儲(chǔ)NSKeyValueObservance對象芦圾,經(jīng)過測試和調(diào)試蛾派,發(fā)現(xiàn)這個(gè)數(shù)組存儲(chǔ)的需要監(jiān)聽的對象中,監(jiān)聽了幾個(gè)屬性个少,如果監(jiān)聽兩個(gè)洪乍,數(shù)組中就是2個(gè)對象。
比如這是監(jiān)聽兩個(gè)屬性狀態(tài)下的數(shù)組


4夜焦,NSKeyValueObservance屬性簡單說明
_observer屬性:里面放的是監(jiān)聽屬性的通知這壳澳,也就是當(dāng)屬性改變的時(shí)候讓哪個(gè)對象執(zhí)行observeValueForKeyPath的對象。
_property 里面的NSKeyValueProperty NSKeyValueProperty存儲(chǔ)的有keyPath,其他屬性我們用不到茫经,暫時(shí)就不說了巷波。


5,拿出keyPath
這時(shí)候思路就有了卸伞,首先拿出_observances數(shù)組抹镊,然后遍歷拿出里面_property對象里面的NSKeyValueProperty下的一個(gè)keyPath,然后進(jìn)行判斷需要?jiǎng)h除或添加的keyPath是否一致荤傲,然后再次判斷傳遞過來的observer和監(jiān)聽的是否一致垮耳,然后分別進(jìn)行處理就行了。


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 observer:observer]) {
        [self removeDasen:observer forKeyPath:keyPath];
    }
}

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

// 進(jìn)行檢索獲取Key
- (BOOL)observerKeyPath:(NSString *)key observer:(id )observer
{
    id info = self.observationInfo;
    NSArray *array = [info valueForKey:@"_observances"];
    for (id objc in array) {
        id Properties = [objc valueForKeyPath:@"_property"];
        id newObserver = [objc valueForKeyPath:@"_observer"];
        
        NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
        if ([key isEqualToString:keyPath] && [newObserver isEqual:observer]) {
            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

感謝各位閱讀氨菇,有什么補(bǔ)充的希望大家提出來。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末妓湘,一起剝皮案震驚了整個(gè)濱河市查蓉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌榜贴,老刑警劉巖豌研,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妹田,死亡現(xiàn)場離奇詭異,居然都是意外死亡鹃共,警方通過查閱死者的電腦和手機(jī)鬼佣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來霜浴,“玉大人晶衷,你說我怎么就攤上這事∫趺希” “怎么了晌纫?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長永丝。 經(jīng)常有香客問我锹漱,道長,這世上最難降的妖魔是什么慕嚷? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任哥牍,我火速辦了婚禮,結(jié)果婚禮上喝检,老公的妹妹穿的比我還像新娘嗅辣。我一直安慰自己,他們只是感情好蛇耀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布辩诞。 她就那樣靜靜地躺著,像睡著了一般纺涤。 火紅的嫁衣襯著肌膚如雪译暂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天撩炊,我揣著相機(jī)與錄音外永,去河邊找鬼。 笑死拧咳,一個(gè)胖子當(dāng)著我的面吹牛伯顶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播骆膝,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼祭衩,長吁一口氣:“原來是場噩夢啊……” “哼阅签!你這毒婦竟也來了掐暮?” 一聲冷哼從身側(cè)響起政钟,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤樟结,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后精算,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瓢宦,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡灰羽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谦趣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疲吸。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖前鹅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情舰绘,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布捂寿,位于F島的核電站孵运,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏治笨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一旷赖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧等孵,春花似錦、人聲如沸果录。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽糖声。三九已至分瘦,卻和暖如春琉苇,著一層夾襖步出監(jiān)牢的瞬間嘲玫,已是汗流浹背并扇。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留土陪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓鬼雀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親源哩。 傳聞我的和親對象是個(gè)殘疾皇子鸦做,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 最近項(xiàng)目中處理kvo 的時(shí)候赁还,遇到一個(gè)問題:當(dāng)我操作的時(shí)候旨枯,會(huì)發(fā)現(xiàn)kvo 釋放的時(shí)候矢炼,會(huì)崩潰, 崩潰日志如下: /...
    那是一陣清風(fēng)_徐來閱讀 5,428評論 0 11
  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,139評論 30 470
  • 上半年有段時(shí)間做了一個(gè)項(xiàng)目夷陋,項(xiàng)目中聊天界面用到了音頻播放胰锌,涉及到進(jìn)度條骗绕,當(dāng)時(shí)做android時(shí)候處理的不太好资昧,由于...
    DaZenD閱讀 3,017評論 0 26
  • http://www.devstore.cn/essay/essayInfo/6525.html【原文地址】 序言...
    起名好難_fz閱讀 645評論 1 1
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉格带,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,709評論 0 9