专栏名称: 唤之
目录
相关文章推荐
OSC开源社区  ·  宇树王兴兴早年创业分享引围观 ·  昨天  
程序员的那些事  ·  李彦宏自曝开源真相:从骂“智商税”到送出“史 ... ·  3 天前  
OSC开源社区  ·  大模型训练中的开源数据和算法:机遇及挑战 ·  3 天前  
程序猿  ·  TCP 才不傻! ·  4 天前  
51好读  ›  专栏  ›  唤之

iOS 如何自动移除KVO观察者

唤之  · 掘金  · 程序员  · 2017-12-09 07:47

正文

声明

有人说这是所谓的黑魔法, 本人在此声明: 本项目无任何黑魔法, 对原代码无任何侵害, 只是对注册方法的封装.

问题

我们都知道, 使用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 . 那如何保持不为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);
}

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







请到「今天看啥」查看全文