最近项目中处理kvo 的时候,遇到一个问题:当我操作的时候,会发现kvo 释放的时候,会崩溃, 崩溃日志如下:
/*Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer
经过反复研究,发现了错误的原因,并且找到解决错误的办法
下面我将介绍一下我的思路:(慢慢来 跟着我的思路走)
1.我在AppDelegate里面添加一个属性
@property(nonatomic,copy)NSString *kvoState;/* 测试kvo设置的一个字段 */
- (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"];
}
TestViewController)里面,然后在TestViewController里面,点击button ,在这个button 的点击事件里面去执行下面的代码:(特地演示错误)
-(void)buttonAction{
SecondViewController *secondVC = [[SecondViewController alloc]init];/*执行此行代码回报上述的错误*/
[self.navigationController popViewControllerAnimated:YES];
}
-(void)dealloc
{
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
[appDelegate removeObserver:self forKeyPath:@"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
+ (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];
}
(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;
}
#import
@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;
}
return self;
}
@end
#import
@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个对象。
比如这是监听两个属性状态下的数组
+ (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;
}