欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS自动移除KVO观察者的实现方法

程序员文章站 2023-12-19 21:54:22
问题 kvo即:key-value observing, 直译为:基于键值的观察者。 它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。 简单的说就是每次...

问题

kvo即:key-value observing, 直译为:基于键值的观察者。 它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。 简单的说就是每次指定的被观察的对象的属性被修改后,kvo就会自动通知相应的观察者了。

kvo的优点:当有属性改变,kvo会提供自动的消息通知。 这样开发人员不需要自己去实现这样的方案:每次属性改变了就发送消息通知。 这是kvo机制提供的最大的优点。 因为这个方案已经被明确定义,获得框架级支持,可以方便地采用。 开发人员不需要添加任何代码,不需要设计自己的观察者模型,直接可以在工程里使用。 其次,kvo的架构非常的强大,可以很容易的支持多个观察者观察同一个属性,以及相关的值。

但我们都知道, 使用kvo模式, 对某个属性进行监听时, observer 需要在必要的时刻进行移除, 否则 app 必然会 crash. 这个问题有点烦人, 因为偶尔会忘记写移除 observer 的代码...

我一直想要这样一个效果:

只管监听, 并处理监听方法. 不去分心, 管何时移除 observer , 让其能够适时自动处理.

所幸, 它能够实现, 先预览一下:

@interface nsobject (sjobserverhelper)

- (void)sj_addobserver:(nsobject *)observer forkeypath:(nsstring *)keypath;

@end

@interface sjobserverhelper : nsobject
@property (nonatomic, unsafe_unretained) id target;
@property (nonatomic, unsafe_unretained) id observer;
@property (nonatomic, strong) nsstring *keypath;
@property (nonatomic, weak) sjobserverhelper *factor;
@end

@implementation sjobserverhelper
- (void)dealloc {
 if ( _factor ) {
 [_target removeobserver:_observer forkeypath:_keypath];
 }
}
@end

@implementation nsobject (observerhelper)

- (void)sj_addobserver:(nsobject *)observer forkeypath:(nsstring *)keypath {
 
 [self addobserver:observer forkeypath:keypath options:nskeyvalueobservingoptionnew context:nil];
 
 sjobserverhelper *helper = [sjobserverhelper new];
 sjobserverhelper *sub = [sjobserverhelper new];
 
 sub.target = helper.target = self;
 sub.observer = helper.observer = observer;
 sub.keypath = helper.keypath = keypath;
 helper.factor = sub;
 sub.factor = helper;
 
 const char *helpekey = [nsstring stringwithformat:@"%zd", [observer hash]].utf8string;
 objc_setassociatedobject(self, helpekey, helper, objc_association_retain_nonatomic);
 objc_setassociatedobject(observer, helpekey, sub, objc_association_retain_nonatomic);
}

@end

项目源码

下面来说说一步一步的实现吧:

初步思路实现:

我们都知道, 对象被释放之前, 会调用dealloc方法, 其持有的实例变量也会被释放.

我就这样想, 在监听注册时, 为self和observer关联个临时对象, 当两者在释放实例变量时, 我借助这个时机, 在临时对象的dealloc方法中, 移除observer就行了.

想法很好, 可总不能每个类里都加一个临时对象的属性吧. 那如何在不改变原有类的情况下, 为其关联一个临时对象呢?

关联属性

不改变原有类, 这时候肯定是要用category了, 系统框架里面有很多的分类, 并且有很多的关联属性, 如下图 uiview 头文件第180行:

iOS自动移除KVO观察者的实现方法

依照上图, 我们先看一个示例, 为nsobject的添加一个category, 并添加了一个property, 在.m中实现了它的setter和getter方法.

#import <objc/message.h>
@interface nsobject (associate)
@property (nonatomic, strong) id tmpobj;
@end
@implementation nsobject (associate)
static const char *testkey = "testkey";
- (void)settmpobj:(id)tmpobj {
 // objc_setassociatedobject(id object, const void *key, id value, objc_associationpolicy policy)
 objc_setassociatedobject(self, testkey, tmpobj, objc_association_retain_nonatomic);
}
- (id)tmpobj {
 // objc_getassociatedobject(id object, const void *key)
 return objc_getassociatedobject(self, testkey);
}
@end

很明确, objc_setassociatedobject 便是关联属性的setter方法, 而objc_getassociatedobject便是关联属性的getter方法. 最需要关注的就是setter方法, 因为我们要用来添加关联属性对象.

初步思路探索

初步尝试:

既然属性可以随时使用objc_setassociatedobject关联了, 那我就尝试先为self关联一个临时对象, 在其dealloc中, 将observer移除.

@interface sjobserverhelper : nsobject
@property (nonatomic, weak) id target;
@property (nonatomic, weak) id observer;
@property (nonatomic, strong) nsstring *keypath;
@end
@implementation sjobserverhelper
- (void)dealloc {
 [_target removeobserver:_observer forkeypath:_keypath];
}
@end
- (void)addobserver {
 nsstring *keypath = @"name";
 [_xiaom addobserver:_observer forkeypath:keypath options:nskeyvalueobservingoptionnew context:nil]; 
 sjobserverhelper *helper_obj = [sjobserverhelper new];
 helper_obj.target = _xiaom;
 helper_obj.observer = _observer;
 helper_obj.keypath = keypath;
 const char *helpekey = [nsstring stringwithformat:@"%zd", [_observer hash]].utf8string;
 // 关联
 objc_setassociatedobject(_xiaom, helpekey, helper_obj, objc_association_retain_nonatomic);
}

于是, 美滋滋的运行了一下程序, 当将_xiaom 置为 nil 时, 砰 app crash......

reason: 'an instance 0x12cd1c370 of class person was deallocated while key value observers were still registered with it.

分析: 临时对象的dealloc, 确确实实的跑了. 为什么会还有registered? 于是我尝试在临时对象的dealloc中, 打印实例变量target, 发现其为nil. 好吧, 这就是crash问题原因!

尝试 unsafe_unretained

通过上面操作, 我们知道self在被释放之前, 会先释放其持有的关联属性, self并未完全释放, 可在临时对象中target却成了nil. 同时self还是有效的, 那如何保持不为nil呢?

我们看看oc中的两个修饰符weak与unsafe_unretained:

  • weak: 持有者不会对目标进行retain, 当目标销毁时, 持有者的实例变量会被置空
  • unsafe_unretained: 持有者不会对目标进行retain, 当目标释放后, 持有者的实例变量还会依然指向之前的内存空间(野指针)

由上, unsafe_unretained很好的解决了我们的问题. 于是我做了如下修改:

@interface sjobserverhelper : nsobject
@property (nonatomic, unsafe_unretained) id target;
@property (nonatomic, unsafe_unretained) id observer;
@property (nonatomic, strong) nsstring *keypath;
@end

再次运行程序, 还行, 观察者移除了.

最终实现

还存在的问题

目前, 我们只是实现了, 如何在self释放的时候, 移除自己身上的observer.

但如果observer提前释放了呢?

而添加关联属性, 两者还不能同时持有临时对象, 否则临时对象也不会及时的释放.

好吧, 既然一个不行, 那就各自关联一个:

- (void)addobserver {
 .....  
 sjobserverhelper *helper_obj = [sjobserverhelper new];
 sjobserverhelper *sub_obj = [sjobserverhelper new];
 sub_obj.target = helper_obj.target = _xiaom;
 sub_obj.observer = helper_obj.observer = _observer;
 sub_obj.keypath = helper_obj.keypath = keypath;
 const char *helpekey = [nsstring stringwithformat:@"%zd", [_observer hash]].utf8string;
 // 关联
 objc_setassociatedobject(_xiaom, helpekey, helper_obj, objc_association_retain_nonatomic);
 // 关联
 objc_setassociatedobject(_observer, helpekey, sub_obj, objc_association_retain_nonatomic);
}

如上, 仔细想想, 存在一个很明显的问题, 两个关联属性释放的同时, 进行了两次观察移除的操作. 为避免这个问题, 我又做了如下修改:

@interface sjobserverhelper : nsobject
@property (nonatomic, unsafe_unretained) id target;
@property (nonatomic, unsafe_unretained) id observer;
@property (nonatomic, strong) nsstring *keypath;
@property (nonatomic, weak) sjobserverhelper *factor; // 1. 新增一个 weak 变量
@end

@implementation sjobserverhelper
- (void)dealloc {
 if ( _factor ) {
  [_target removeobserver:_observer forkeypath:_keypath];
 }
}
@end

- (void)addobserver {
 ..... 
 sjobserverhelper *helper_obj = [sjobserverhelper new];
 sjobserverhelper *sub_obj = [sjobserverhelper new];
 sub_obj.target = helper_obj.target = _xiaom;
 sub_obj.observer = helper_obj.observer = _observer;
 sub_obj.keypath = helper_obj.keypath = keypath;
 // 2. 互相 weak 引用
 helper_obj.factor = sub_obj; 
 sub_obj.factor = helper_obj;
 const char *helpekey = [nsstring stringwithformat:@"%zd", [_observer hash]].utf8string;
 // 关联
 objc_setassociatedobject(_xiaom, helpekey, helper_obj, objc_association_retain_nonatomic);
 // 关联
 objc_setassociatedobject(_observer, helpekey, sub_obj, objc_association_retain_nonatomic);
}

在之前的操作中, 我们知道, weak 修饰的变量, 在目标释放时,持有者的实例变量都会自动置为nil, 因此如上dealloc方法中, 我们只需要判断weak引用的实例变量factor是否为空即可.

抽取

以上操作, 我们就可以解决偶尔忘记写移除observer的代码了. 现在只需要把实现抽取出来, 做成一个通用的工具方法:

我新建了一个nsobject的category, 并添加了一个方法, 如下:

iOS自动移除KVO观察者的实现方法

然后将上述的实现进行了整合放到了.m中:

iOS自动移除KVO观察者的实现方法

到此, 以后只需要调用- (void)sj_addobserver:(nsobject *)observer forkeypath:(nsstring *)keypath;这个方法即可, 移除就交给临时变量自己搞定.

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。

上一篇:

下一篇: