有人说这是所谓的黑魔法, 本人在此声明: 本项目无任何黑魔法, 对原代码无任何侵害, 只是对注册方法的封装.git
咱们都知道, 使用KVO模式, 对某个属性进行监听时, Observer 须要在必要的时刻进行移除, 不然 App 必然会 Crash. 这个问题有点烦人, 由于偶尔会忘记写移除 Observer 的代码...github
我一直想要这样一个效果: 只管监听, 并处理监听方法. 不去分心, 管什么时候移除 Observer , 让其可以适时自动处理.bash
所幸, 它可以实现, 先预览一下:markdown
@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
方法, 其持有的实例变量也会被释放.oop
我就这样想, 在监听注册时, 为self
和Observer
关联个临时对象, 当二者在释放实例变量时, 我借助这个时机, 在临时对象的dealloc
方法中, 移除Observer
就好了.atom
想法很好, 可总不能每一个类里都加一个临时对象的属性吧. 那如何在不改变原有类的状况下, 为其关联一个临时对象呢?spa
不改变原有类, 这时候确定是要用Category
了, 系统框架里面有不少的分类, 而且有不少的关联属性, 以下图 UIView 头文件第180行: 指针
依照上图, 咱们先看一个示例, 为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问题缘由!
经过上面操做, 咱们知道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...