------更新------:
之前沒有判斷observer是否一致,有個(gè)別情況會(huì)無法處理,所以更新添加了observer判斷
一、使用場(chǎng)景
有時(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)行操作。
- 方案一 :利用 @try @catch
- 方案二 :利用 模型數(shù)組 進(jìn)行存儲(chǔ)記錄
- 方案二 :利用 observationInfo 里私有屬性
(1) 方案一(只能針對(duì)刪除多次KVO的情況下)
利用 @try @catc
不得不說這種方法真是很Low,但是很簡(jiǎn)單就可以實(shí)現(xiàn)。
這種方法只能針對(duì)多次刪除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è)簡(jiǎn)單的方法:給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è)對(duì)象。
2,是在監(jiān)聽的keyPath是什么。
第二步 存儲(chǔ)思路
1,我們需要一個(gè)模型用來存儲(chǔ)
哪個(gè)對(duì)象執(zhí)行了addObserver、監(jiān)聽的KeyPath是什么。
2,我們需要一個(gè)數(shù)組來存儲(chǔ)這個(gè)模型。
第三步 進(jìn)行存儲(chǔ)
1,利用runtime 攔截到對(duì)象和keyPath,創(chuàng)建模型然后進(jìn)行賦值模型相應(yīng)的屬性。
2,然后存儲(chǔ)進(jìn)數(shù)組中去。
第三步 存儲(chǔ)之前的檢索處理
1,在存儲(chǔ)之前,為了防止多次addObserver相同的屬性,這個(gè)時(shí)候我們就可以,遍歷數(shù)組,取出每個(gè)一個(gè)模型,然后取出模型中的對(duì)象,首先判斷對(duì)象是否一致,然后判斷keypath是否一致2,對(duì)于添加KVO監(jiān)聽:如果不一致那么就執(zhí)行利用交換后方法執(zhí)行addObserver方法。
3,對(duì)于刪除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];
// 如果沒有注冊(cè),那么才進(jìn)行注冊(cè)
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 里私有屬性
第一步 簡(jiǎn)單介紹下observationInfo屬性
1,只要是繼承與NSObject的對(duì)象都有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)識(shí)信息對(duì)象,信息包含了每個(gè)監(jiān)聽的觀察者,注冊(cè)時(shí)設(shè)定的選項(xiàng)等。
@property (nullable) void *observationInfo;
6,observationInfo結(jié)構(gòu) (箭頭所指是我們等下需要用到的地方)
第二步 實(shí)現(xiàn)方案思路
1,通過私有屬性直接拿到當(dāng)前對(duì)象所監(jiān)聽的keyPath,和observer
2,判斷keyPath是否有無,和observer是否對(duì)應(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對(duì)象,經(jīng)過測(cè)試和調(diào)試,發(fā)現(xiàn)這個(gè)數(shù)組存儲(chǔ)的需要監(jiān)聽的對(duì)象中,監(jiān)聽了幾個(gè)屬性,如果監(jiān)聽兩個(gè),數(shù)組中就是2個(gè)對(duì)象。
比如這是監(jiān)聽兩個(gè)屬性狀態(tài)下的數(shù)組
4,NSKeyValueObservance屬性簡(jiǎn)單說明
_observer屬性:里面放的是監(jiān)聽屬性的通知這,也就是當(dāng)屬性改變的時(shí)候讓哪個(gè)對(duì)象執(zhí)行observeValueForKeyPath的對(duì)象。
_property 里面的NSKeyValueProperty NSKeyValueProperty存儲(chǔ)的有keyPath,其他屬性我們用不到,暫時(shí)就不說了。
5,拿出keyPath
這時(shí)候思路就有了,首先拿出_observances數(shù)組,然后遍歷拿出里面_property對(duì)象里面的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ǔ)充的希望大家提出來。