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对应存储项;
相关推荐
2501_9160088922 分钟前
全面介绍Fiddler、Wireshark、HttpWatch、SmartSniff和firebug抓包工具功能与使用
android·ios·小程序·https·uni-app·iphone·webview
Haha_bj4 小时前
Flutter ——flutter_screenutil 屏幕适配
android·ios
Haha_bj4 小时前
Flutter ——device_info_plus详解
android·flutter·ios
山水域9 小时前
SKAdNetwork 6.0 深度实战:多窗口转化值(Conversion Value)建模与数据分层架构
ios
JavinLu10 小时前
ios 配置了代理且使用 chls.pro/ssl 下载不了证书,无法弹出下载证书的提示问题
网络协议·ios·ssl
G311354227312 小时前
免费苹果 Plist 文件在线制作 iOS IPA 安装工具
ios
2401_8322981013 小时前
免费p12证书在线检测iOS苹果证书状态一键查询
ios
符哥200814 小时前
Swift 开发 iOS App 过程中写自定义控件的归纳总结
ios·cocoa·swift
pop_xiaoli15 小时前
effective-Objective-C 第二章阅读笔记
笔记·学习·ios·objective-c·cocoa
未来侦察班1 天前
一晃13年过去了,苹果的Airdrop依然很坚挺。
macos·ios·苹果vision pro