iOS探究使用Block方式实现一对多回调能力

新星杯·14天创作挑战营·第18期 10w+人浏览 140人参与

一、前言

在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 存储,利用 NSMapTableNSMapTableWeakMemory 特性存储实例对象(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对应存储项;
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值