问题引述:使用Observer时遇到Cannot remove an observer XXXXXXXX for the key path “aObserverName” from XXXXXXXX because it is not registered as an observer

方案一::利用 @try @catch(只能针对删除多次KVO的情况下)

利用 @try @catc:这种方法真是很Low,人就会有问题,不过很简单就可以实现。(对于初学者来说,如果不怕麻烦,确实可以使用这种方法)。这种方法只能针对多次删除KVO的处理,原理就是try catch可以捕获异常,不让程序catch。这样就实现了防止多次删除KVO。

1
2
3
4
5
6
@try {
[appDelegate removeObserver:self forKeyPath:@"kvoState"];
}
@catch (NSException *exception) {
NSLog(@"多次删除kvo 报错了");
}

方法功能拓展:

有个简单的方法:给NSObject 增加一个分类,然后利用Runtime 交换系统的 removeObserver方法,在里面添加 @try @catch。


方案二::利用 模型数组 进行存储记录
  • 第一步 利用交换方法,拦截到需要的东西
  1. 是在监听哪个对象。
  2. 是在监听的keyPath是什么。
  • 第二步 存储思路
  1. 我们需要一个模型用来存储哪个对象执行了addObserver、监听的KeyPath是什么。
  2. 我们需要一个数组来存储这个模型。
  • 第三步 进行存储
  1. 利用runtime 拦截到对象和keyPath,创建模型然后进行赋值模型相应的属性。
  2. 然后存储进数组中去。
  • 存储之前的检索处理
  1. 在存储之前,为了防止多次addObserver相同的属性,这个时候我们就可以,遍历数组,取出每个一个模型,然后取出模型中的对象,首先判断对象是否一致,然后判断keypath是否一致
  2. 对于添加KVO监听:如果不一致那么就执行利用交换后方法执行addObserver方法。
  3. 对于删除KVO监听: 如果一致那么我们就执行删除监听,否则不执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
+ (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);


SEL addSel = @selector(addObserver:forKeyPath:options:context:);
SEL myaddSel = @selector(addDasen:forKeyPath:options:context:);
Method systemAddMethod = class_getClassMethod([self class],addSel);
Method DasenAddMethod = class_getClassMethod([self class], myaddSel);
method_exchangeImplementations(systemAddMethod, DasenAddMethod);
}

#pragma mark - 第二种方案,利用私有属性
// 交换后的方法
- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath{

NSMutableArray *Observers = [YLObserver sharedInstanceObserve];
YLObserverData *userPathData = [self observerKeyPath:keyPath];
// 如果有该key值那么进行删除 ,对于删除KVO监听: 如果一致那么我们就执行删除监听,否则不执行
if (userPathData) {
[Observers removeObject:userPathData];
@try {
//如果没有写@try @catch 的话,在 dealloc 中,那个被监听的对象(appdelegate)必须要全局变量
[self removeDasen:observer forKeyPath:keyPath];
}
@catch (NSException *exception) {
}
}
return;
}


交换后的方法
/*
第一步 利用交换方法,拦截到需要的东西
1,是在监听哪个对象。
2,是在监听的keyPath是什么。
*/
- (void)addDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{

/*
第二步 存储思路
1,我们需要一个模型用来存储哪个对象执行了addObserver、监听的KeyPath是什么。
2,我们需要一个数组来存储这个模型。
*/
YLObserverData *userPathData= [[YLObserverData alloc]initWithObjc:self key:keyPath];
NSMutableArray *Observers = [YLObserver sharedInstanceObserve];

// 如果没有注册,那么才进行注册 第三步 存储之前的检索处理
if (![self observerKeyPath:keyPath]) {
/*
第三步 进行存储
1,利用runtime 拦截到对象和keyPath,创建模型然后进行赋值模型相应的属性。
2,然后存储进数组中去。
*/
[Observers addObject:userPathData];
[self addDasen:observer forKeyPath:keyPath options:options context:context];
}
}


// 进行检索,判断是否已经存储了该Key值
- (YLObserverData *)observerKeyPath:(NSString *)keyPath{

NSMutableArray *Observers = [YLObserver sharedInstanceObserve];
for (YLObserverData *data in Observers) {
if ([data.objc isEqual:self] && [data.keyPath isEqualToString:keyPath]) {
return data;
}
}

return nil;
}

方案三:利用 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了,有知道原因的可以给我说下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
+ (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);


SEL addSel = @selector(addObserver:forKeyPath:options:context:);
SEL myaddSel = @selector(addDasen:forKeyPath:options:context:);
Method systemAddMethod = class_getClassMethod([self class],addSel);
Method DasenAddMethod = class_getClassMethod([self class], myaddSel);
method_exchangeImplementations(systemAddMethod, DasenAddMethod);
}

#pragma mark - 第二种方案,利用私有属性
// 交换后的方法
- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath{

// 如果有该key值那么进行删除
if ([self observerKeyPath:keyPath]) {
@try {
//如果没有写@try @catch 的话,在 dealloc 中,那个被监听的对象(appdelegate)必须要全局变量
[self removeDasen:observer forKeyPath:keyPath];
}
@catch (NSException *exception) {
}
}
}

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

//_observer属性:里面放的是监听属性的通知,也就是当属性改变的时候让哪个对象执行observeValueForKeyPath的对象。
NSArray *array = [info valueForKey:@"_observances"];

for (id objc in array) {
//_property 里面的NSKeyValueProperty存储的有keyPath,其他属性我们用不到,暂时就不说了。
id Properties = [objc valueForKeyPath:@"_property"];
NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];

if ([key isEqualToString:keyPath]) {
return YES;
}
}
return NO;
}

Demo地址:https://github.com/yuanliangYL/YLCrashhandler