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

一、前言

在iOS开发中,封装工具类,管理类及实现数据中心等场景时,需要实现一对多的回调能力;常见如通知、KVO、Swift还有个Combine,或者扩展实现多代理,这些实现方式有个问题是代码的连续性不足,这里探讨一种使用block的实现方式,可有效利用Block能将触发回调的代码和回调处理逻辑集中在同一代码块中,避免逻辑分散,且上下文友好、语法简洁、方便阅读;

二、探究实现

先假定一个简单场景,方便说明实现细节:

实现一个数据中心类,提供一个注册方法,外部可传入一个对象和一个block块,对象和block一一对应;

当数据中心内部接收到数据变动时,对应执行执行其block块内部逻辑;以此实现注册到此数据中心的所有对象对应block都可以被触发执行;

1. 先确定 h文件,实现注册和清理注册两个方法

一个方法让外部注册回调,可实现数据中心数据变化,触发其项目中不同的多处注册的回调执行其对应block代码块逻辑:
- (void)registerTarget:(NSObject *)target withDataChangeBlock:(RPDataChangeBlock)block;

一个方法主动清理注册,方便外部随业务主动及时清理注册数据回调;
- (void)removerRegisterTarget:(NSObject *)target;

objectivec 复制代码
#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 不被提前释放。

objectivec 复制代码
@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. 实现注册方法的存储
objectivec 复制代码
- (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 时可自动跳过已释放的实例。

objectivec 复制代码
- (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步中的这个注册方法,额外添加动态绑定逻辑;

objectivec 复制代码
// 使用runtime动态绑定,需import runtime库
#import <objc/runtime.h>
// 定义一个用于绑定销毁回调的关联Key
static const void *kRPTargetDeallocCallbackKey = &kRPTargetDeallocCallbackKey;
objectivec 复制代码
- (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对应存储项;
相关推荐
TheNextByte18 小时前
iPhone短信备份与恢复:3种最佳方法及短信备份与恢复应用
ios·iphone
2501_916008898 小时前
iOS 应用发布流程中常被忽视的关键环节
android·ios·小程序·https·uni-app·iphone·webview
ForteScarlet8 小时前
Kotlin 2.3.0 现已发布!又有什么好东西?
android·开发语言·后端·ios·kotlin
往来凡尘1 天前
Flutter运行iOS26真机的两个问题
flutter·ios
普通网友1 天前
Objective-C 类的方法重载与重写:区别与正确使用场景
开发语言·ios·objective-c
denggun123451 天前
卡顿监测原理
macos·ios·xcode
@大迁世界1 天前
iOS 26.2 引入三种全新 iPhone 自定义方式
ios·iphone
Sheffi661 天前
iOS 触摸事件完整传递链路:Hit-Test 全流程深度解析
macos·ios·cocoa