一、前言
在iOS开发中,封装工具类,管理类及实现数据中心等场景时,需要实现一对多的回调能力;常见如通知、KVO、Swift还有个Combine,或者扩展实现多代理,这些实现方式有个问题是代码的连续性不足,这里探讨一种使用block的实现方式,可有效利用Block能将触发回调的代码和回调处理逻辑集中在同一代码块中,避免逻辑分散,且上下文友好、语法简洁、方便阅读;
二、探究实现
先假定一个简单场景,方便说明实现细节:
实现一个数据中心类,提供一个注册方法,外部可传入一个对象和一个block块,对象和block一一对应;
当数据中心内部接收到数据变动时,对应执行执行其block块内部逻辑;以此实现注册到此数据中心的所有对象对应block都可以被触发执行;
1. 先确定 h文件,实现注册和清理注册两个方法
一个方法让外部注册回调,可实现数据中心数据变化,触发其项目中不同的多处注册的回调执行其对应block代码块逻辑:
- (void)registerTarget:(NSObject *)target withDataChangeBlock:(RPDataChangeBlock)block;
一个方法主动清理注册,方便外部随业务主动及时清理注册数据回调;
- (void)removerRegisterTarget:(NSObject *)target;
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// 数据源变化时的回调Block
typedef void (^RPDataChangeBlock)(id _Nullable data);
@interface RPDataCenterManager : NSObject
// >>================= 重点方法 =================>>
/// 注册实例对象和数据变化回调
/// @param target 实例对象
/// @param block 数据变化回调
- (void)registerTarget:(NSObject *)target withDataChangeBlock:(RPDataChangeBlock)block;
/// 移除注册的对象记录
/// @param target 实例对象
- (void)removerRegisterTarget:(NSObject *)target;
// <<================= 重点方法 =================<<
/// 单例(保证全局唯一的监听和存储)
+ (instancetype)sharedInstance;
/// mock方法-模拟数据源变化
/// @param newData 新的数据源
- (void)mockMethodIsCaptureExternalNewData:(id _Nullable)newData;
@end
NS_ASSUME_NONNULL_END
2. 存储注册的对象及对应block的方式;
既然要实现有数据变化时的回调,那注册的对象和其对应的block就要存储起来,等数据变化时,去读取保存的block去执行;
使用键值对的方式存储对象与其对应的block,这里使用NSMapTable 存储,利用 NSMapTable 的 NSMapTableWeakMemory 特性存储实例对象(key 弱引用),保证实例释放时 key 自动置空,这样有数据变化去遍历 NSMapTable 时可自动跳过已释放的实例。
NSPointerFunctionsWeakMemory:key(实例对象)为弱引用,实例释放后 key 自动失效;
NSPointerFunctionsStrongMemory:value(block)为强引用,保证 block 不被提前释放。
@interface RPDataCenterManager ()
/// 存储target(弱引用)和对应的block(强引用)
@property (nonatomic, strong) NSMapTable<id, RPDataChangeBlock> *targetBlockMap;
@end
@implementation RPDataCenterManager
#pragma mark - 单例初始化
+ (instancetype)sharedInstance {
static RPDataCenterManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (instancetype)init {
if (self = [super init]) {
// 配置NSMapTable:key弱引用(实例释放自动置空),value强引用
self.targetBlockMap = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsWeakMemory
valueOptions:NSPointerFunctionsStrongMemory];
}
return self;
}
@end
3. 实现注册方法的存储
- (void)registerTarget:(NSObject *)target withDataChangeBlock:(RPDataChangeBlock)block {
if (!target || !block) return;
// 存储target和block到NSMapTable
[self.targetBlockMap setObject:block forKey:target];
}
4. 实现数据变化时触发所有block回调
因NSMapTable设置的key为NSPointerFunctionsWeakMemory,故遍历 NSMapTable 时可自动跳过已释放的实例。
- (void)mockMethodIsCaptureExternalNewData:(id _Nullable)newData {
// 遍历NSMapTable,执行所有有效的block
NSEnumerator *enumerator = [self.targetBlockMap keyEnumerator];
id target;
while ((target = [enumerator nextObject])) {
RPDataChangeBlock block = [self.targetBlockMap objectForKey:target];
if (block) {
block(newData);
}
}
}
5. 优化NSMapTable存储自动清除时机
以上实现有个问题,NSMapTable存储的记录虽然可以保证外部注册target释放时,对应block不再被执行,但存储记录依然存在与NSMapTable内部,依然占用内存;
故需想办法监听外部target释放时,及时清理掉NSMapTable内存储的对应记录,还有考虑底代码的解构,最好manager内部实现监听,避免提供额外方法造成耦合;
基于以上此处考虑利用Runtime的动态绑定来实现以上这个需求:
通过 objc_setAssociatedObject 给实例对象绑定一个销毁回调 block;
实例对象的 dealloc 执行时,关联的 block 会被触发,从而清理 NSMapTable 中的对应记录;
绑定策略使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC,保证回调 block 生命周期与实例一致。
重新看第3步中的这个注册方法,额外添加动态绑定逻辑;
// 使用runtime动态绑定,需import runtime库
#import <objc/runtime.h>
// 定义一个用于绑定销毁回调的关联Key
static const void *kRPTargetDeallocCallbackKey = &kRPTargetDeallocCallbackKey;
- (void)registerTarget:(NSObject *)target withDataChangeBlock:(RPDataChangeBlock)block {
if (!target || !block) return;
// 1. 先清理该target的旧关联(避免重复绑定)
[self removeDeallocCallbackForTarget:target];
// 2. 存储target和block到NSMapTable
[self.targetBlockMap setObject:block forKey:target];
// 3. 动态绑定销毁回调到target
__weak typeof(self) weakSelf = self;
void (^deallocCallback)(void) = ^{
// 实例释放时,清理NSMapTable中对应的记录
[weakSelf.targetBlockMap removeObjectForKey:target];
};
objc_setAssociatedObject(target,
kRPTargetDeallocCallbackKey,
deallocCallback,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#pragma mark - 私有方法
/// 移除target的销毁回调关联
- (void)removeDeallocCallbackForTarget:(id)target {
objc_setAssociatedObject(target,
kRPTargetDeallocCallbackKey,
nil,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)dealloc {
// 清空所有关联
[self.targetBlockMap removeAllObjects];
}
@end
三、总结
综上所述:
- 业务方不同业务代码调用注册方法,添加需执行的block;
- 当数据中心接收到数据变化时,进行数据处理然后遍历NSMapTable,执行所有block;
- 当业务注册target释放时,触发内部runtime动态绑定实现的类似watchDog功能,清理NSMapTable对应存储项;
447

被折叠的 条评论
为什么被折叠?



