• iOS 如何自动移除KVO观察者

    发布:51Code 时间: 2018-04-23 13:32

  • 问题 我们都知道, 使用KVO模式, 对某个属性进行监听时, Observer 需要在必要的时刻进行移除, 否则 App 必然会 Crash. 这个问题有点烦人, 因为偶尔会忘记写移除 Observer 的代码... 我一直想要...

  • 问题

    我们都知道, 使用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行:

    ex.png

    依照上图, 我们先看一个示例, 为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, 并添加了一个方法, 如下:

    ex2.png

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

    ex3.png

    到此, 以后只需要调用- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;这个方法即可, 移除就交给临时变量自己搞定.

    结语: 能够看到这里, 老铁是真爱了, 可以帮小弟去点个Star.

    Over...

    作者:changsanjiang
    来源:http://www.jianshu.com/p/11bb1dcfb07e
    博为峰对此不表示赞同或者反对,也不为其提供证明,仅供阅读者交流参考。
    上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编caoxiaoyan@51testing.com,我们将立即处理

  • 上一篇:iOS 自动打包探索

    下一篇:转型项目经理,鬼知道我经历了什么

网站导航
Copyright(C)51Code软件开发网 2003-2018 , 沪ICP备16012939号-1