一个关于单例的 Block 回调设计
关于这个设计思路在一年前就有了,只是最近一年都在写 CI,所以一直没填这个坑。关于如何传值,如何回调的思路能找到很多,但是关于单例如何用 Block 回调,没细心去找,所以不知道有没有和我想到一块儿的。
环境信息
iOS 11.3
Xcode 9.3
在写单例的时候,难免会遇上一些状态回调。比如蓝牙 SDK 需要回调蓝牙的状态,又比如网络 SDK 需要回调网络状态等等。
AFNetworking 中的网络回调
之所以思考 Block 回调的问题,是因为发现 AFNetworking 的 AFNetworkReachabilityManager
的 setReachabilityStatusChangeBlock
网络状态回调设置以后有被覆盖的风险:
AFNetworkReachabilityManager *manager = [AFNetworkReachabilityManager sharedManager]; |
上面的代码中,manager
是一个单例,对 Block 进行赋值以后,可在网络状态发生改变时,在当前类中做些处理。而在 setReachabilityStatusChangeBlock
实现中发现,仅仅是将 Block 赋值给了普通变量,而不是集合类型的变量:
- (void)setReachabilityStatusChangeBlock:(void (^)(AFNetworkReachabilityStatus status))block { |
这个问题在 AFNetworking 的 issue 上也有人提出(#3014),但是解决方案很蜜汁。一种是在回调当中发起通知,另一种已经合入 3.0.0 版本,做法是…给 AFNetworkReachabilityManager
新增了一个创建普通实例对象的方法(MR #3111):
/** |
看了下这部分代码关联的 issue,都是和 Block 被覆盖相关的,除了 “保证原有 API 不变,向下兼容” 这个理由外,我实在是找不出其他的解释了。
我有个大胆的想法
基于上面的问题,是否能设计一个不被覆盖的 Block 回调呢?
集合
覆盖的根本问题是因为 AFNetworkReachabilityManager
的 shareManager
是个单例,而接收 Block 又是普通对象,如果换成集合类型,这个问题就解决了。集合选择主要关注两个问题:
- 有序?无序?
- strong?weak?
有序无序可以根据业务需求来选择。而对于是否持有,可以到实际场景里面看下:
- (void)addNetworkStatusCallback:(dispatch_block_t)callback { |
假如选择持有,也就是用 NSMutableDictionary
,会引入新问题:callback 何时释放?显然,当注册回调的实例释放以后,callback 也应该被移除,选择 NSMutableDictionary
在此并不是好的方案。
那么选择不持有,用 NSMapTable
+ NSPointerFunctionsWeakMemory
这个组合:
- (void)addNetworkStatusCallback:(dispatch_block_t)callback { |
结果 block 刚 set 就被释放了。
到此,【集合的选用问题】变成了【应该由谁持有 Block 的问题】。
Associated Object
在我看来,由谁注册回调,就由谁管理,单例仅负责维护映射关系,所以代码变成了:
- (void)addObserver:(id)observer callback:(dispatch_block_t)callback { |
通过 objc_setAssociatedObject
将 callback
绑定给 observer
,而单例的 NSMapTable
中,Key 与 Value 属性都是 weak,仅维护 observer
与 callback
的映射,谁也不持有。比如在 ViewController 中:
[[ObserverSingleton shareInstance] addObserver:self callback:^{ |
此时,callback
由 self
持有,单例中的 self.table
结构为:
{ |
这样处理,降低了调用者不 remove 就会造成内存泄漏或者野指针的风险,但是依然不完美:在调用者看来,self
并没有持有 callback
,即使 callback
中直接使用 self
也不会造成循环引用。但这是一个误区,在单例中,我们实际将 callback
绑定给了 self
。所以 callback
中还是必须使用 weakSelf
才行。目前没想到好的方案。
移除
移除分为两步,一个是重置绑定,一个是移除映射关系:
- (void)removeObserver:(id)observer { |
关于代码中的 @"UUID"
,在实现的时候实际上用的是地址:
[[NSString stringWithFormat:@"%p", &observer] UTF8String] |
绑定的对象需要手动释放吗?
在 objc 源码中,能依次找到 NSObject
的 dealloc
→ _objc_rootDealloc
→ rootDealloc
→ object_dispose
→ objc_destructInstance
。其中 objc_destructInstance
的实现如下:
void *objc_destructInstance(id obj) |
其中调用的移除关联对象的方法 _object_remove_assocations
中最后一行为:
for_each(elements.begin(), elements.end(), ReleaseValue()); |
最终调用 objc_release
释放对象:
static void releaseValue(id value, uintptr_t policy) { |
得益于 dealloc
时调用的 _object_remove_assocations
函数,所以关联的对象,是不需要手动释放的。
最后
关于一对多的消息发送,是否适合用 Block,这里不做讨论。仅从 Block 是否可以一对多的角度来看,这种做法是可行的。
2018.10.30 更新
监听 dealloc
关于释放时机,也有大佬提出了另外的想法「为什么不直接监听 dealloc
呢?」,优势是可以解决 observer
持有 block
,而调用者不知道,在 block
内部调用 self
而造成循环引用的问题。劣势并不突出,增加代码量姑且算一个 😄,总的来说,还是利大于弊的。当然,大佬还提出「可以 NSObject
都监听 dealloc
,直接写成 Category」,关于这点我倒是觉得没必要。
采用监听 dealloc
的方案,改动主要有两处。一处是持有关系的变更,一处是实现 dealloc
的监听与处理。
持有关系
既然采用监听 observer
释放来进行 block
的释放,那么 block
的持有者就可以是 NSMapTable
:
// key 保持不变,依然是 weak,不干扰 observer 的生命周期 |
如何监听 dealloc
如何监听 observer
的释放呢?思考这个问题的时候,让我想起了 ReactiveCocoa 中 @onExit
的实现:引入其他变量。
因为无法直接知道 observer
的释放,但是能通过给它绑定一个对象,通过监听绑定对象的释放,从而得知 observer
的释放时机:
@interface DeallocWatcher: NSObject |
当 DeallocWatcher
被释放时,触发初始化注册的 deallocCallback
回调。通过将 DeallocWatcher
绑定给 observer
,可以得到 observer
的释放时机:
- (void)addObserver:(id)observer callback:(dispatch_block_t)callback { |