我们都知道, 使用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
想法很好, 可总不能每个类里都加一个临时对象的属性吧. 那如何在不改变原有类的情况下, 为其关联一个临时对象呢
不改变原有类, 这时候肯定是要用
了, 系统框架里面有很多的分类, 并且有很多的关联属性, 如下图 UIView 头文件第180行:
- Category
依照上图, 我们先看一个示例, 为
的添加一个
- 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.'
分析: 临时对象的
, 确确实实的跑了. 为什么会还有registered? 于是我尝试在
- dealloc
的
- 临时对象
中, 打印实例变量
- dealloc
, 发现其为nil. 好吧, 这就是Crash问题原因!
- target
通过上面操作, 我们知道
在被释放之前, 会先释放其持有的关联属性, 在析构期间, 可以确定
- self
还是存在的, 并未完全释放, 可在临时对象中
- self
却成了
- target
. 那如何保持不为nil呢
- nil
我们看看OC中的两个修饰符
与
- weak
:
- unsafe_unretained
由上,
很好的解决了我们的问题. 于是我做了如下修改:
- 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
然后将上述的实现进行了整合放到了
中:
- .m
到此, 以后只需要调用
这个方法即可, 移除就交给临时变量自己搞定.
- - (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
结语: 能够看到这里, 老铁是真爱了, 可以帮小弟去点个Star. Over...
来源: https://juejin.im/post/5a2b6436f265da43163cfe8f