06-主题|事件响应者链@iOS-响应者链传递方式与编程模式详解

3 阅读29分钟

本文以响应者链传递为研究对象,系统介绍在 iOS 开发中实现「事件/回调沿链或向多对象传递」的常见编程模式:系统原生的 nextResponder 传递、Delegate(含 Delegate 数组 多委托分发)、Block/闭包函数封装、以及快速枚举与 for 循环等遍历式传递。与「链的构成」配合阅读可参见 03-响应者链与 nextResponder 详解


一、为何研究「传递方式」

响应者链解决的是「事件交给谁、如何向上传」的问题;在业务层我们还会用 DelegateBlock/闭包封装函数遍历回调 等方式把「事件/结果」从一处传到另一处。将这些传递方式作为研究对象,便于在「系统链」与「业务回调」之间做统一理解与选型:何时用链、何时用 delegate、何时用闭包、何时用「数组 + 循环」等。

1.1 传递方式总览(思维导图)

mindmap
  root((传递方式))
    系统链
      nextResponder
      触摸 Action 编辑菜单
    Delegate
      一对一 协议
      Delegate 数组 多委托分发
    Block 闭包
      完成回调 单次通知
      按钮 onTap
    函数封装
      可注入 可测
      EventHandler
    遍历 for
      多监听 拦截器链
      责任链

二、系统原生:nextResponder 链式传递

2.1 机制回顾

UIKit 的响应者链通过 next(nextResponder)把未处理的事件单向、逐节点传递:第一响应者 → next → next.next → … → nil。每个节点要么处理,要么交给下一个,不复制、不广播

flowchart LR
    A[第一响应者] --> B[next]
    B --> C[next]
    C --> D[nil]

特点:链式、单目标、系统驱动。详见 03-响应者链与 nextResponder 详解

2.2 与后文模式的对比

传递方式方向/形态典型用途
nextResponder单向链,系统逐节点转发触摸、Action(target=nil)、编辑菜单
Delegate一对一,调用方 → 委托方TableView 数据与点击、自定义控件回调
Delegate 数组一对多,调用方 → 遍历 delegates 依次通知多模块同协议监听、广播式通知但需协议约束
Block/闭包调用时执行一段逻辑完成回调、按钮点击、异步结果
函数封装把「处理逻辑」当参数传递高阶函数、统一封装转发
快速枚举/for 循环对集合逐项调用多监听者、拦截器链、责任链

三、Delegate(委托)模式

3.1 定义与角色

Delegate 是一种一对一的传递方式:持有者(如 View/Control)不直接处理业务,而是把事件通过协议方法交给委托对象(如 ViewController)处理。事件流向:视图/控件 → delegate 实现方

  • 委托方:定义协议(protocol),在适当时机调用 delegate?.method?(...)
  • 受托方:实现协议,提供具体逻辑。

3.2 与响应者链的关系

  • TableView 的 tableView(_:didSelectRowAt:)、Cell 的点击上报,常由 ViewController 作为 delegate 接收,VC 本身也在响应者链上,但这里的「传递」是协议调用,不是 next 链。
  • 当 Cell 内按钮用 target=nil 时,才会走响应者链找 target;若用 delegate,则是显式把「谁来处理」绑定到 delegate 上,不依赖链。

3.3 代码示例(Swift)

// 委托方:定义协议,在事件发生时调用 delegate
protocol ItemCellDelegate: AnyObject {
    func itemCell(_ cell: ItemCell, didTapButton sender: UIButton)
}

class ItemCell: UITableViewCell {
    weak var delegate: ItemCellDelegate?
    @objc private func buttonTapped() {
        delegate?.itemCell(self, didTapButton: button)
    }
}

// 受托方:ViewController 实现协议,接收「传递」过来的事件
class ListViewController: UIViewController, ItemCellDelegate {
    func itemCell(_ cell: ItemCell, didTapButton sender: UIButton) {
        // 处理点击,如路由、弹窗等
    }
}

商用场景示例:商品列表/订单列表 Cell 内「去支付」「查看物流」「删除」等按钮,由 VC 作为 delegate 接收,统一做路由、埋点、弹窗;或自定义表头/筛选栏把「筛选条件变更」通过 delegate 交给 VC 刷新列表。

特点:一对一、协议约束、弱引用避免循环;适合「视图把事件交给其拥有者/控制器」的场景。

3.4 Delegate 数组:多委托事件分发

Delegate 数组指持有多个遵循同一协议的对象([Protocol]),在事件发生时遍历该数组,对每个元素调用协议方法,从而把同一事件分发给多个接收者。形态是「一对多」,但约束仍是协议,与「for 遍历闭包/Handler」的区别在于:每个监听者都是协议类型(常为 weak 引用),类型清晰、无闭包捕获。

对比项单 DelegateDelegate 数组
数量一个委托方多个委托方,同协议
调用delegate?.method?(...)delegates.forEach { $0.method?(...) } 或 for 循环
顺序按数组顺序依次调用,可控制
典型用途列表 Cell → VC、控件 → 拥有者多模块监听同一事件且需协议约束(如生命周期、登录状态变更)

代码示例(Swift)

protocol DataSourceDidUpdateDelegate: AnyObject {
    func dataSourceDidUpdate(_ source: DataManager)
}

class DataManager {
    private var delegates: [WeakRef<DataSourceDidUpdateDelegate>] = []
    func addDelegate(_ d: DataSourceDidUpdateDelegate) {
        delegates.append(WeakRef(d))
    }
    func removeDelegate(_ d: DataSourceDidUpdateDelegate) {
        delegates.removeAll { $0.value === d }
    }
    private func notifyUpdate() {
        delegates.removeAll { $0.value == nil }
        delegates.forEach { $0.value?.dataSourceDidUpdate(self) }
    }
}
// WeakRef 为对 AnyObject 的弱引用封装,避免数组强引用导致不释放

与「for 循环遍历 Handler」的异同:二者都是 O(n) 遍历;Delegate 数组的每项是协议类型,弱引用、无闭包分配,类型与责任更清晰;Handler 数组可以是闭包或协议,更灵活但闭包有捕获与生命周期问题。需要多对象按协议接收、且希望避免闭包时,优先 Delegate 数组。

商用场景示例:首页多个区域(行情、持仓、资讯)都需在「用户登录成功」或「数据刷新」时更新,用同一协议 HomeSectionRefreshDelegate,DataManager 持有一个 delegate 数组,在登录回调或下拉刷新时遍历通知;或播放器持有多个 PlaybackStateDelegate,在播放/暂停/进度变化时依次通知多个 UI 或统计模块。


四、Block(Objective-C)与闭包(Swift)

4.1 定义与角色

Block(OC)与闭包(Swift)都是一段可传递、可延迟执行的代码。调用方在合适的时机执行该 block/闭包,即完成「把结果或事件传递回调用方或指定逻辑」。

  • 持有方:保存 block/闭包,在事件完成时调用(如 completion(result))。
  • 传递方:传入一段逻辑,不关心谁最终执行,只关心「何时、以何参数」被调用。

4.2 与响应者链的关系

  • 响应者链传递的是事件对象(如 touches、action),沿 next 逐级转发。
  • Block/闭包传递的是一段处理逻辑:由控件/网络层在完成后调用,不经过 next。二者互补:链负责「谁有权处理系统事件」,闭包负责「完成后通知谁」。

4.3 常见用法

场景用法
按钮点击button.onTap = { [weak self] in self?.handleTap() }(或 addTarget 里封装)
异步完成fetchData { result in ... }
动画结束UIView.animate(..., completion: { _ in ... })

4.4 代码示例(Swift)

// 封装「传递」为闭包属性,由外部注入逻辑
class ActionButton: UIButton {
    var onTap: (() -> Void)?
    @objc private func didTap() {
        onTap?()
    }
}

// 使用:事件「传递」到闭包
button.onTap = { [weak self] in
    self?.navigateToDetail()
}

商用场景示例:支付结果、提交订单、上传完成等异步结果用 completion 闭包回调;弹窗「确定/取消」用闭包传递用户选择;网络请求成功/失败用 (Result<T, Error>) -> Void 传递。

Objective-C Block 示例(与 Swift 闭包对应):

// 定义
typedef void(^OnTapBlock)(void);
@property (nonatomic, copy) OnTapBlock onTap;

// 触发
if (self.onTap) self.onTap();

// 使用
__weak typeof(self) wself = self;
cell.onTap = ^{
    [wself navigateToDetail];
};

特点:灵活、可内联、注意 [weak self] 避免循环引用;适合单次回调、完成块、简单事件上报。


五、函数封装与高阶传递

5.1 定义与角色

函数封装指把「如何处理」抽象成函数类型(或闭包),作为参数或属性传递,在需要时统一调用。这样「传递」的是处理逻辑本身,而非固定写死调用谁。

  • 封装方:提供 (Event) -> Void(T) -> Bool 等类型,在内部某处调用。
  • 注入方:传入具体实现(函数/闭包),实现「谁来处理」的绑定。

5.2 与链式传递的类比

  • 响应者链:每个节点有固定的 next,系统按链调用。
  • 函数封装:把「下一跳逻辑」或「最终处理逻辑」以函数/闭包的形式注入,调用方只负责在适当时机执行,不关心具体是谁。

5.3 代码示例(Swift)

// 事件处理类型:封装为函数类型,便于注入与测试
typealias EventHandler = (UIEvent) -> Bool

class CustomView: UIView {
    var eventHandler: EventHandler?
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let event = event else { return }
        if eventHandler?(event) == true {
            return // 已处理
        }
        next?.touchesEnded(touches, with: event) // 未处理则交给链
    }
}

// 使用:把「处理」从外部注入
customView.eventHandler = { event in
    // 自定义逻辑,返回 true 表示消费
    return true
}

高阶示例:对「多个处理函数」做顺序调用(见下节),即用数组保存多个「处理函数」,用 for 循环依次调用,形成责任链/拦截器链


六、快速枚举与 for 循环:多对象遍历传递

6.1 定义与角色

当需要把同一事件/消息传给多个接收者,或按固定顺序依次尝试处理时,常用集合 + 遍历实现:

  • 快速枚举(ObjC:for (id obj in array);Swift:for x in array)遍历数组/集合。
  • for 循环(索引或 iterator)对「拦截器列表、监听者列表」逐项调用,直到某者处理或全部调用完毕。

与 nextResponder 的对比:

  • nextResponder:单链、每节点一个 next、系统驱动。
  • 遍历传递:显式维护「接收者列表」或「处理函数列表」,用 for 控制顺序与短路(如「有一个返回 true 即停止」)。

6.2 典型场景

场景实现方式
多监听者listeners.forEach { $0.onEvent(event) }
拦截器链for interceptor in interceptors { if interceptor.handle(event) { return } }
责任链按顺序尝试 handler,直到有一个返回「已处理」

6.3 伪代码:拦截器链(for 循环传递)

函数 dispatchEvent(event):
    for interceptor in interceptors:
        if interceptor.handle(event) == true:
            return   // 已处理,停止传递
    defaultHandler(event)   // 无人处理则走默认

6.4 代码示例(Swift)

// 定义「处理者」协议
protocol EventHandling {
    func handle(_ event: UIEvent) -> Bool
}

// 持有处理者数组,按顺序传递
class EventDispatcher {
    var handlers: [EventHandling] = []
    func dispatch(_ event: UIEvent) {
        for handler in handlers {
            if handler.handle(event) { return }
        }
    }
}

// 使用:多个对象依次有机会处理,类似「链」但由数组+for 驱动
dispatcher.handlers = [logger, validator, router]
dispatcher.dispatch(event)

商用场景示例:统一路由前的拦截器链(登录校验 → 权限 → 埋点 → 跳转),用 for 循环顺序执行,某一步返回「已处理」则短路;或多监听者(如多个模块监听「用户登录成功」),用 listeners.forEach { $0.onLogin() } 广播。

6.4 泳道图:三种传递方式的参与角色

flowchart LR
    subgraph Delegate
        D1[View 触发]
        D2[delegate 方法]
        D3[VC 处理]
        D1 --> D2
        D2 --> D3
    end
    subgraph Delegate数组
        DA1[事件发生]
        DA2[for d in delegates]
        DA3[依次 d.method]
        DA1 --> DA2
        DA2 --> DA3
    end
    subgraph Block闭包
        B1[异步完成]
        B2[执行 closure]
        B3[调用方逻辑]
        B1 --> B2
        B2 --> B3
    end
    subgraph 遍历for
        F1[事件发生]
        F2[for handler in handlers]
        F3[依次调用]
        F1 --> F2
        F2 --> F3
    end

6.5 与系统响应者链的对比

维度系统 nextResponder自维护数组 + for
结构树/链结构,next 由视图层级决定线性列表,顺序由数组决定
谁维护系统业务代码
典型用途触摸、Action、编辑菜单拦截器、多监听、责任链

6.6 Delegate 数组 与 for 遍历 Handler 的对比

维度Delegate 数组for 遍历 Handler/闭包
元素类型协议类型(多为 weak 引用)协议或闭包;闭包有捕获
单次派发成本O(delegate 数量)O(handler 数量)
内存无闭包分配,弱引用易释放若为闭包则可能大量捕获、循环引用
顺序与短路通常全量通知,不短路可设计为「有一个处理则 return」的链式
适用多模块同协议监听、需类型约束拦截器链(短路)、或任意回调列表

七、传递方式总览与选型

flowchart TB
    subgraph 传递方式
        A[nextResponder 链]
        B[Delegate]
        B2[Delegate 数组]
        C[Block/闭包]
        D[函数封装]
        E[快速枚举/for 循环]
    end
    subgraph 典型用途
        A --> A1[系统事件 单链向上]
        B --> B1[视图→控制器 一对一]
        B2 --> B2a[多委托 同协议 依次通知]
        C --> C1[完成回调 单次通知]
        D --> D1[可注入逻辑 可测试]
        E --> E1[多监听 拦截器链]
    end
方式关系适用
nextResponder系统链,单向触摸、target=nil、编辑菜单
Delegate一对一,协议TableView/Cell、自定义控件上报
Delegate 数组一对多,协议数组遍历多模块同协议监听、避免闭包时的广播
Block/闭包调用时执行按钮回调、异步 completion、简单上报
函数封装逻辑可注入统一转发、可测、高阶逻辑
快速枚举/for多对象顺序调用监听者列表、拦截器链、责任链

7.1 商用场景与传递方式选型

商用场景推荐传递方式说明
列表 Cell 内按钮点击DelegateCell 不持有 VC,弱引用 delegate,VC 统一路由/埋点
支付/提交/上传完成Block/闭包单次异步结果,completion(result) 回调
弹窗确定/取消闭包onConfirm: () -> Void, onCancel: () -> Void
路由前登录/权限校验for 循环拦截器链顺序执行,未通过则中断,通过则继续下一环
多模块监听同一事件for 遍历 listeners / Delegate 数组广播式;若需协议约束、弱引用、无闭包,用 Delegate 数组
多模块同协议接收(如登录成功刷新)Delegate 数组持有一组 weak 协议引用,遍历调用,类型清晰、易释放
系统编辑菜单复制/粘贴响应者链链上查找 canPerformAction / target

八、不同传递方式的性能分析

事件传递方式的选型会直接影响调用开销、内存占用、主线程耗时与可预测性。在高频交互或对延迟敏感的行业中,选错方式可能带来卡顿、内存泄漏或不可接受的响应延迟。本节先拆解影响性能的要点,再从创建对象、内存分配、寻址三个底层维度对比各方式;最后给出各方式性能优劣对比技术选型策略

8.1 影响性能的要点(拆解)

以下要点共同决定「某种传递方式」在具体场景下的性能表现;选型时需逐项对照。

要点含义对性能的影响
调用路径长度从事件发生到「真正处理」所经过的节点数或调用层数路径越长,单次延迟与 CPU 时间越大;长链/长数组在热路径上会放大延迟
单次派发复杂度一次事件触发时,派发逻辑的时间复杂度(如 O(1) / O(n))O(1) 可预测、适合高频;O(n) 随 n 增长,n 大或频率高时易成为瓶颈
堆分配与捕获是否在派发路径上分配堆内存(如闭包)、是否捕获外部变量热路径上频繁分配会带来 ARC 压力与缓存不友好;捕获易导致生命周期与循环引用问题
引用关系与生命周期持有方对被通知方的引用是强引用还是弱引用、是否易释放强引用或闭包捕获易导致对象无法释放、内存泄漏;弱引用 + 协议更易控制生命周期
主线程耗时派发与各接收方处理是否发生在主线程、总耗时是否可控主线程上 O(n) 派发 + 多个重处理会直接导致卡顿、丢帧;需控制单帧内派发量与单次处理耗时
可预测性与上限延迟与耗时是否有明确上界、是否随数据量/监听者数恶化金融/医疗等场景需要「最坏情况」可估;不可预测的 O(n) 或闭包创建不利于保障 SLA
扩展性监听者/处理者数量增加时,单次派发与内存如何变化广播式随 n 线性变慢;单通道或固定链长扩展性更好

小结低延迟、高频、主线程敏感场景应优先 O(1) 派发、无热路径堆分配、弱引用多接收者场景需控制 n 或改为单通道聚合;可审计、可预测场景应避免「依赖链碰巧传到」、闭包未调用等非确定性。

8.2 从对象创建、内存分配与寻址看性能

创建对象分配内存寻址三个底层维度拆解,能更清晰看出各传递方式在热路径上的成本差异,以及为何在高频或延迟敏感场景下选型会带来明显性能差别。

8.2.1 对象创建(Object Creation)

传递方式谁在何时创建每次事件是否新建对象对性能的影响
nextResponder视图树与 next 链由系统在布局时建立,响应者对象本身已存在;事件派发阶段不创建新对象无创建成本,热路径零分配
Delegate委托方与 delegate 引用在绑定阶段设置(如 cell 的 delegate = vc);派发时仅解引用已有指针无创建成本,适合高频
Delegate 数组数组与 weak 包装在注册时创建;派发时只遍历已有引用;单次派发不创建新对象注册时一次性成本;派发路径无新建
Block/闭包闭包在「设置回调」时创建(如 button.onTap = { ... });若在 cellForRow 等热路径内每次赋值则每次新建闭包对象视使用方式;热路径内每次赋值即新建一个闭包对象热路径每次创建会带来堆分配 + 捕获,是主要性能风险
函数封装若注入的是闭包则同 Block;若是函数引用则无额外对象同 Block 或无取决于实现
for 循环遍历handler 数组在注册时建立;若元素是闭包则每个元素对应一次闭包创建派发时不创建;若 handler 列表在热路径重建则同 Block派发阶段无新建;维护的若是闭包列表则创建发生在注册侧

要点热路径上是否「每事件或每帧新建对象」直接决定 ARC 压力与缓存行为。nextResponder、Delegate、Delegate 数组在派发时都不创建新对象;Block/闭包一旦在 cellForRow、scrollViewDidScroll 等处每次赋值,就会每次新建闭包对象,应改为 Delegate 或复用同一闭包。

8.2.2 内存分配(Memory Allocation)

传递方式堆 vs 栈何时分配、分配多少对性能的影响
nextResponder链节点为已有 view/VC,无因「传递机制」产生的额外堆分配事件派发阶段零堆分配无 ARC 压力、无碎片,适合高频
Delegate仅一个指针(通常 weak)存储在持有方;不分配新块绑定 delegate 时无额外堆分配;派发时零分配极低内存 footprint
Delegate 数组数组本身堆分配(若用 Array);元素为 weak 包装可能占少量堆(视实现)注册阶段一次分配;派发时无分配成本在初始化与扩容;派发路径无分配
Block/闭包闭包在 Swift/OC 中多为堆分配(捕获上下文时);捕获变量越多,闭包对象越大每次创建闭包即可能一次堆分配 + 捕获区热路径频繁创建会导致分配峰值、ARC 写屏障、可能的内存碎片
函数封装若为闭包则同 Block;若为函数指针则无额外堆同 Block 或无取决于实现
for 循环遍历handler 数组堆分配;若元素为闭包则每个闭包堆分配注册/添加时分配;派发时通常无新分配与 Delegate 数组类似;元素为闭包时总占用更高

要点派发热路径上是否触发堆分配是核心。nextResponder、Delegate 在派发时零堆分配;Delegate 数组、for 遍历在派发时也无分配,成本在注册与容器;Block/闭包在每次创建时可能堆分配,故「热路径避免每事件新建闭包」是硬约束。

8.2.3 寻址(Addressing / Lookup)

传递方式如何找到「处理者」寻址次数与复杂度对性能的影响
nextResponder沿 next 指针逐节点走链,直到某节点处理或链尾O(链长) 次指针解引用与方法派发;链长由视图层级决定链越长,首帧响应延迟与 CPU 时间越大;寻址路径不可由业务完全控制
Delegate直接通过单一指针(如 delegate 属性)找到对象,再发协议消息O(1):一次指针解引用 + 一次方法调用延迟可预测、缓存友好,适合关键路径
Delegate 数组通过数组下标顺序访问每个元素,再对每个 delegate 发消息O(n):n 次数组访问 + n 次指针解引用与调用;n = delegate 数量随 n 线性增长;n 小则可控,n 大或频率高则成为瓶颈
Block/闭包通过持有的闭包对象引用直接调用,无「查找谁来处理」的过程O(1):一次闭包调用(若闭包已存在);创建时无「寻址」但有一次分配调用成本低;成本主要在「创建时」的分配与捕获
函数封装通过持有的函数类型/闭包引用直接调用O(1)(调用阶段)同 Delegate 或 Block,取决于实现
for 循环遍历通过数组迭代依次访问 handler,再调用O(n):n 次访问 + n 次调用(或短路前 k 次)与 Delegate 数组类似;若需「找到第一个能处理的」则可能早退,均摊寻址次数取决于数据

要点寻址复杂度决定「从事件发生到调用到处理者」的 CPU 时间与可预测性。O(1) 寻址(Delegate、已存在的闭包)延迟稳定;O(链长)O(n) 随规模增长,在高频或延迟敏感场景需严格控制链长或 n。

8.2.4 三维度综合与选型含义

flowchart LR
    subgraph 创建对象
        C1[next/Delegate/Delegate数组 派发时无新建]
        C2[Block 热路径每次新建 则成本高]
    end
    subgraph 内存分配
        M1[派发路径零分配 最优]
        M2[闭包创建 堆分配]
    end
    subgraph 寻址
        A1[O1 一次解引用 可预测]
        A2[On 或 O链长 随规模增长]
    end
    C1 --> M1
    C2 --> M2
    A1 --> 高频优选
    A2 --> 控n或控链长
若关注…优先避免
创建/分配派发路径零创建、零堆分配:DelegatenextResponder(系统)、Delegate 数组(派发时)热路径每次新建 Block/闭包、在 cellForRow 等处每行新建闭包
寻址O(1) 一次解引用:Delegate、已持有的闭包/函数引用O(n) 遍历、O(链长) 长链在关键路径上;若必须用则控制 n 或链长
综合高频/关键路径:Delegate(O(1) 寻址 + 零创建 + 零分配);多接收者且要协议约束:Delegate 数组(n 小,派发时无创建无分配)热路径:新建闭包、长链依赖、大 n 遍历广播

8.3 各方式性能优劣对比

从上述要点出发,对各传递方式做优劣势归纳,便于直接对比。

传递方式性能优势性能劣势关键约束
nextResponder系统实现高度优化、无额外堆分配、无业务层引用负担链长不可控时延迟与链长成正比;业务无法决定「谁先收到」链结构由视图层级决定,适合系统事件
DelegateO(1) 派发、一次指针解引用、weak 引用无循环、主线程耗时极低、延迟可预测仅支持单一接收者;扩展为多接收者需改 Delegate 数组或其它方式一对一、路径固定、适合关键路径
Delegate 数组无闭包分配、协议 + 弱引用、类型清晰、生命周期易控单次 O(n),n 大或频率高时主线程压力线性增长;无短路则每人都执行监听者数量宜少、中低频或单次派发 n 可控
Block/闭包调用成本与普通函数相当;灵活、可内联业务逻辑创建时可能堆分配 + 捕获,热路径频繁创建会拉高 CPU/内存;捕获不当易循环引用适合低频、一次性回调;热路径避免每事件新建闭包
函数封装注入一次、多次调用无重复分配时与 Delegate 接近若实现为闭包则同 Block;若为函数引用则接近 Delegate取决于实现是闭包还是稳定函数引用
for 循环遍历顺序与短路可自定义(如拦截器链可早退);结构清晰O(n) 派发;若元素为闭包则同 Block 的内存与捕获问题;n 大或高频时易卡顿拦截器链 n 小且可短路;广播式 n 需严格控制或改用单通道

8.4 技术选型策略(按场景决策)

事件特征业务约束做选型,可按下表逐项确认,再锁定方式。

决策维度若为「是」或「该情况」推荐倾向说明
接收者数量仅 1 个Delegate 或显式 targetO(1)、路径固定、无歧义
少量(如 3~10)、且需协议约束Delegate 数组无闭包、弱引用、类型清晰;n 小则 O(n) 可接受
多且不可控 或 高频避免遍历广播;改为单通道/单处理者或聚合后一次通知控制主线程单次派发量与 n
事件频率高频(如每帧、每 tick、滚动)Delegate 或单处理者;禁止在热路径上 for 遍历大量监听者或每事件新建闭包热路径上 O(1)、零分配
中低频(如登录成功、支付完成)Delegate / Delegate 数组 / 闭包 均可,按接收者数量与协议需求选单次 O(n) 或一次闭包调用可接受
延迟与可预测性关键路径、延迟敏感(如下单、撤单)Delegate显式 target,保证 O(1)、不依赖长链不依赖 target=nil 长链、不依赖遍历查找
需可审计、责任唯一Delegate 或显式 target,避免「链上碰巧能响应的对象」谁处理可追溯
是否需要顺序/短路需固定顺序且「有一个处理即停止」for 循环拦截器链,按顺序执行、早退 return校验链、权限链
需固定顺序且「全部执行」Delegate 数组 或 for 遍历,顺序由数组保证多模块同协议刷新
生命周期与内存担心循环引用、希望监听方易释放Delegate / Delegate 数组(weak 协议);慎用闭包,若用必须 [weak self]弱引用 + 协议 无闭包捕获
单次异步结果、调用方保证释放前完成闭包 可接受,保证 completion 必达且 weak 打破循环网络/支付回调
是否系统事件触摸、Action、编辑菜单系统 nextResponder 链;业务层用 Delegate/显式 target 补足「谁处理」链由系统驱动,业务不重复造链

选型口诀(便于记忆)

  • 一对一、要稳要快 → Delegate。
  • 一对多、要协议、要省内存 → Delegate 数组(n 小)。
  • 一次回调、异步结果 → 闭包(必达 + weak)。
  • 多步校验、顺序+短路 → for 拦截器链。
  • 高频、多接收者 → 不做遍历广播,改单通道或聚合。

选型决策流程(简化)

flowchart TD
    A[事件需分发] --> B{接收者数量?}
    B -->|1 个| C[Delegate / 显式 target]
    B -->|多个| D{是否高频?}
    D -->|是| E[单通道/单处理者 或 聚合后一次通知]
    D -->|否| F{需协议约束、弱引用?}
    F -->|是| G[Delegate 数组 n 小]
    F -->|否| H{需顺序+短路?}
    H -->|是| I[for 拦截器链]
    H -->|否| J[for 遍历 或 闭包列表]
    K[单次异步结果] --> L[闭包 必达+weak]

8.5 性能维度对比表(汇总)

传递方式单次调用开销内存特点主线程影响适用频率
nextResponder沿链逐节点查找,O(链长);系统实现高度优化,无额外堆分配无闭包/block 捕获,无额外持有链越长,首响应者越晚找到,触摸到响应的理论延迟略增系统驱动,每次触摸/Action 一次
Delegate一次指针解引用 + 协议方法调用,O(1);无闭包创建weak 引用,不增加引用计数极低,路径固定适合高频(如列表滚动中的点击)
Delegate 数组遍历数组调用每个 delegate 的协议方法,O(delegate 数);无闭包创建数组内多为 weak 包装,不增加被引用方计数随 delegate 数量线性增长;数量少时可控,多时同 for 广播中低频、监听者数量可控(如数个模块)
Block/闭包调用成本与普通函数相当;但创建闭包可能分配堆、捕获上下文捕获 self/变量易形成循环引用;大量创建增加 GC/ARC 压力若在热路径上频繁创建闭包(如 cellForRow 内),会加剧内存与 CPU 峰值适合低频、一次性回调(如网络完成、弹窗)
函数封装与闭包类似,注入一次、多次调用时无重复分配取决于实现是闭包还是函数引用同 Delegate/闭包,取决于调用点中低频、可复用处理逻辑
for 循环遍历O(监听者数量);每次派发遍历整个列表需维护 handler 数组;若 handler 是闭包则同闭包监听者很多时,一次派发可能触发大量回调,主线程易卡顿监听者少时可控;广播式慎用于高频事件

8.6 典型性能问题与规避

  • nextResponder:链过长(视图层级过深)时,hit-test 与链传递的节点数增加,可考虑扁平化视图层级、避免无用中间 view。
  • Delegate:几乎无额外性能负担;注意 delegate 为 nil 时的短路,避免无效调用。
  • Delegate 数组:单次派发 O(n),与「for 遍历 listeners」类似;优势是无闭包分配、弱引用易释放,适合「多模块同协议、数量有限」的广播;高频事件下应控制 delegate 数量或改为单通道聚合。
  • Block/闭包:在 Cell 复用 中为每个 Cell 创建新的闭包会带来大量短期对象,可改为 delegate 或复用同一闭包 + 参数;循环引用 必须用 [weak self] 或弱引用打破。
  • for 循环 / 多监听者高频事件(如行情 tick、滚动)不宜用「遍历所有监听者」广播,应改为单通道、单处理者或合并更新;拦截器链应控制长度并在首条快速短路(如登录校验未通过即 return)。

8.7 性能与选型小结(思维导图)

mindmap
  root((传递方式 性能))
    nextResponder
      O链长 系统优化
      无额外分配
    Delegate
      O1 指针调用
      弱引用 低内存
    Delegate数组
      On 委托数
      无闭包 弱引用
    Block闭包
      创建有分配
      捕获 循环引用风险
    for遍历
      On 监听者数
      高频慎用

九、行业场景与技术选型准确性:金融、票务、医疗

金融股票、票务、医疗等对实时性、一致性、可审计性要求极高的领域,事件传递方式的选型错误可能直接导致下单延迟、订单状态错乱、合规与安全风险。下面用这三类场景说明「选对方式」的重要性。

9.1 金融 / 股票类 App

场景特点:行情 tick 高频更新、下单/撤单要求低延迟且结果可预期、资金与持仓状态必须一致。

风险点错误选型示例正确思路
下单/撤单延迟用「遍历多个监听者」在每次点击时广播,或依赖 target=nil 沿很长响应者链查找,导致首帧响应延迟不可控关键路径Delegate显式 target:下单按钮点击直接交给交易 VC/交易模块,O(1) 调用,延迟可测
行情推送与 UI 更新每笔 tick 都 listeners.forEach { $0.onTick(quote) },监听者过多或单次处理过重导致主线程卡顿、丢帧单通道更新:一个 行情处理者聚合后驱动 UI(如 DataSource 绑定 TableView);或子线程处理 + 主线程一次刷新,避免「多监听者 + 高频」
资金/持仓结果回调用闭包接收下单结果时,若闭包未正确持有或未在完成时调用,用户界面显示「提交中」永不变,实际已成交/已失败单次异步结果Block/闭包 可接受,但必须保证 completion 一定被调用(成功/失败/取消);关键状态变更建议同时走 Delegate 或通知,便于日志与对账

多模块监听:若需「登录成功 / 持仓变更」等通知多个 UI 或统计模块,且希望避免闭包捕获与生命周期问题,可用 Delegate 数组:持有一组 weak 协议引用,遍历调用;协议约束清晰、监听者数量可控时,比「for 遍历闭包列表」更易维护与释放。

结论:金融场景下,核心交易动线优先 Delegate 或显式 target,保证路径固定、延迟可控;异步结果用闭包时需保证必达与幂等处理;高频事件避免「多监听者 for 循环」式广播;多模块同协议监听可选用 Delegate 数组 替代闭包列表。

9.2 票务系统(购票、选座、支付)

场景特点:选座与库存强一致、支付结果与订单状态强一致、高并发下不能重复提交或状态错乱。

风险点错误选型示例正确思路
选座/提交订单Cell 内「选座」按钮用 target=nil,响应者链未到达订单 VC,点击无反应或误触到其他 VCDelegate 明确「Cell → 订单 VC」:谁处理、谁刷新、谁调接口,路径清晰;避免依赖链的「碰巧能传到」
支付结果回调支付 SDK 回调用闭包,但页面已 pop 或对象已释放,闭包未执行或执行时 self 已 nil,订单状态未更新支付/订单结果Delegate(如 PaymentResultDelegate)绑定到订单 VC 或专门的结果处理器,生命周期与页面一致;若用闭包,必须 weak self + 判空,且保证在任意结果下都调用一次 completion
校验链顺序下单前需:登录 → 实名 → 库存 → 风控;若用「多监听者广播」无法保证顺序,可能先扣库存再发现未登录for 循环拦截器链:按固定顺序执行「登录校验 → 实名校验 → 库存校验 → 风控」,任一步未通过即 return,不执行下单;顺序与责任清晰,便于排查与合规

结论:票务场景下,关键操作(选座、下单、支付)Delegate显式回调对象,确保「谁在处理」可追踪;多步校验for 循环拦截器链 保证顺序与短路;支付/订单结果 若用闭包需保证必达与生命周期安全。

9.3 医疗系统(医嘱、用药、审批)

场景特点:操作可审计、责任可追溯、误触或「事件被错误对象处理」可能带来合规与安全风险。

风险点错误选型示例正确思路
关键操作责任归属用药确认、医嘱提交等按钮用 target=nil,事件沿链传递到「碰巧能响应的」某个 VC,日志与审计无法对应到正确业务模块关键操作必须 Delegate显式 target:只有「当前负责该患者/该医嘱的 VC」能处理,便于记录「谁在何时点击了确认」
敏感数据与闭包在闭包中捕获患者 ID、医嘱 ID 等,若闭包被不当持有或泄露,敏感信息随闭包生命周期扩散避免在闭包中长期持有敏感对象;用 Delegate 传递「当前上下文」,由受托方按需取数;若用闭包,仅传必要参数且尽快释放
多步审批/校验提交前需:权限 → 必填项 → 业务规则 → 提交;若用广播或乱序调用,可能绕过某一步for 循环拦截器链 固定顺序:权限 → 校验 → 提交;每步可写审计日志(如「某用户在某时通过某步」),便于合规

结论:医疗场景下,关键操作 必须 Delegate 或显式 target,保证「处理者」唯一、可审计;敏感数据 避免长期被闭包捕获;多步流程for 循环链 保证顺序与可追溯。

9.4 行业场景与选型对照表

行业关键诉求推荐传递方式慎用/禁用
金融/股票低延迟、可预测、结果必达核心路径 Delegate/显式 target;异步结果闭包需必达高频 tick 用 for 广播;长响应者链依赖 target=nil
票务状态一致、不重复提交、支付结果可靠选座/下单/支付 Delegate;校验链 for 顺序执行Cell 内关键按钮 target=nil;支付回调闭包不保证调用
医疗可审计、责任清晰、敏感数据可控关键操作 Delegate;多步流程 for 拦截器链关键操作 target=nil;敏感数据长期闭包捕获

9.5 技术选型准确性的重要性(小结)

  • 选对方式:核心路径用 Delegate / 显式 target,延迟与责任可预期;异步单次结果用 闭包 时保证 completion 必达;多步校验/审批用 for 循环链 保证顺序与可审计。
  • 选错后果:依赖 target=nil + 长链 可能导致关键操作「传不到」或传到错误对象;高频 + 多监听者 for 易导致卡顿与丢帧;闭包未必达循环引用 导致状态不一致或内存泄漏;敏感数据被闭包长期持有 增加合规与泄露风险。

在金融、票务、医疗等强合规、高实时或高一致场景中,事件传递方式本身就是架构约束的一环,需在设计阶段明确「谁处理、何种方式、何种顺序」,并在代码与文档中保持一致。

9.6 行业场景与选型/风险关系(流程图)

flowchart TB
    subgraph 金融股票
        F1[行情 tick 高频]
        F2[下单/撤单 低延迟]
        F3[结果必达]
        F1 --> F_ok[单通道/聚合更新]
        F2 --> F_delegate[Delegate/显式 target]
        F3 --> F_callback[闭包必达 或 Delegate]
    end
    subgraph 票务
        T1[选座/下单]
        T2[支付结果]
        T3[校验顺序]
        T1 --> T_delegate[Delegate 明确 VC]
        T2 --> T_delegate
        T3 --> T_for[for 拦截器链]
    end
    subgraph 医疗
        M1[关键操作 可审计]
        M2[敏感数据]
        M3[多步审批]
        M1 --> M_delegate[Delegate 责任唯一]
        M2 --> M_delegate
        M3 --> M_for[for 链 顺序固定]
    end

十、延伸阅读


参考文献