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:selfforKeyPath:@"kvoState"options:NSKeyValueObservingOptionNewcontext: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:selfforKeyPath:@"kvoState"];
}
3.在第二步之後,我點選一個button ,push 到 另外一個ViewController(TestViewController)裡面,然後在TestViewController裡面,點選button ,在這個button 的點選事件裡面去執行下面的程式碼:(特地演示錯誤)
-(void)buttonAction{
SecondViewController *secondVC = [[SecondViewControlleralloc]init];/*執行此行程式碼回報上述的錯誤*/
[self.navigationControllerpopViewControllerAnimated: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
{
[selfswitchMethod];
}
+ (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
{
[selfswitchMethod];
}
+ (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;
}
}
returnnil;
}
這種情況還需要新建幾個檔案:DSObserver 、ObserverData——————————————————————————————————————————————————————————————#import <Foundation/Foundation.h>
@interface ObserverData : NSObject
@property (nonatomic, strong)id objc;
@property (nonatomic, copy) 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;
}
returnself;
}
@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個物件。
比如這是監聽兩個屬性狀態下的陣列
_observer屬性:裡面放的是監聽屬性的通知這,也就是當屬性改變的時候讓哪個物件執行observeValueForKeyPath的物件。
_property 裡面的NSKeyValueProperty NSKeyValueProperty儲存的有keyPath,其他屬性我們用不到,暫時就不說了。
5,拿出keyPath
這時候思路就有了,首先拿出_observances陣列,然後遍歷拿出裡面_property物件裡面的NSKeyValueProperty下的一個keyPath,然後進行判斷需要刪除或新增的keyPath是否一致,然後分別進行處理就行了。
補充:NSKeyValueProperty我當時測試直接kvc取出來的時候發現取不出來,報錯,後臺直接取keyPath就可以,然後就直接取keyPath了,有知道原因的可以給我說下。
+ (void)load
{
[selfswitchMethod];
}
+ (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;
}
}
returnNO;
}
上述就是這個問題的解決方法 參考人員:tyhgithub地址