本文以响应者链传递为研究对象,系统介绍在 iOS 开发中实现「事件/回调沿链或向多对象传递」的常见编程模式:系统原生的 nextResponder 传递、Delegate(含 Delegate 数组 多委托分发)、Block/闭包、函数封装、以及快速枚举与 for 循环等遍历式传递。与「链的构成」配合阅读可参见 03-响应者链与 nextResponder 详解。
一、为何研究「传递方式」
响应者链解决的是「事件交给谁、如何向上传」的问题;在业务层我们还会用 Delegate、Block/闭包、封装函数、遍历回调 等方式把「事件/结果」从一处传到另一处。将这些传递方式作为研究对象,便于在「系统链」与「业务回调」之间做统一理解与选型:何时用链、何时用 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 引用),类型清晰、无闭包捕获。
| 对比项 | 单 Delegate | Delegate 数组 |
|---|---|---|
| 数量 | 一个委托方 | 多个委托方,同协议 |
| 调用 | 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 内按钮点击 | Delegate | Cell 不持有 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或控链长
| 若关注… | 优先 | 避免 |
|---|---|---|
| 创建/分配 | 派发路径零创建、零堆分配:Delegate、nextResponder(系统)、Delegate 数组(派发时) | 热路径每次新建 Block/闭包、在 cellForRow 等处每行新建闭包 |
| 寻址 | O(1) 一次解引用:Delegate、已持有的闭包/函数引用 | O(n) 遍历、O(链长) 长链在关键路径上;若必须用则控制 n 或链长 |
| 综合 | 高频/关键路径:Delegate(O(1) 寻址 + 零创建 + 零分配);多接收者且要协议约束:Delegate 数组(n 小,派发时无创建无分配) | 热路径:新建闭包、长链依赖、大 n 遍历广播 |
8.3 各方式性能优劣对比
从上述要点出发,对各传递方式做优劣势归纳,便于直接对比。
| 传递方式 | 性能优势 | 性能劣势 | 关键约束 |
|---|---|---|---|
| nextResponder | 系统实现高度优化、无额外堆分配、无业务层引用负担 | 链长不可控时延迟与链长成正比;业务无法决定「谁先收到」 | 链结构由视图层级决定,适合系统事件 |
| Delegate | O(1) 派发、一次指针解引用、weak 引用无循环、主线程耗时极低、延迟可预测 | 仅支持单一接收者;扩展为多接收者需改 Delegate 数组或其它方式 | 一对一、路径固定、适合关键路径 |
| Delegate 数组 | 无闭包分配、协议 + 弱引用、类型清晰、生命周期易控 | 单次 O(n),n 大或频率高时主线程压力线性增长;无短路则每人都执行 | 监听者数量宜少、中低频或单次派发 n 可控 |
| Block/闭包 | 调用成本与普通函数相当;灵活、可内联业务逻辑 | 创建时可能堆分配 + 捕获,热路径频繁创建会拉高 CPU/内存;捕获不当易循环引用 | 适合低频、一次性回调;热路径避免每事件新建闭包 |
| 函数封装 | 注入一次、多次调用无重复分配时与 Delegate 接近 | 若实现为闭包则同 Block;若为函数引用则接近 Delegate | 取决于实现是闭包还是稳定函数引用 |
| for 循环遍历 | 顺序与短路可自定义(如拦截器链可早退);结构清晰 | O(n) 派发;若元素为闭包则同 Block 的内存与捕获问题;n 大或高频时易卡顿 | 拦截器链 n 小且可短路;广播式 n 需严格控制或改用单通道 |
8.4 技术选型策略(按场景决策)
按事件特征与业务约束做选型,可按下表逐项确认,再锁定方式。
| 决策维度 | 若为「是」或「该情况」 | 推荐倾向 | 说明 |
|---|---|---|---|
| 接收者数量 | 仅 1 个 | Delegate 或显式 target | O(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,点击无反应或误触到其他 VC | 用 Delegate 明确「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
十、延伸阅读
- 链的构成与 next 规则:03-响应者链与 nextResponder 详解
- 应用场景与 target=nil、delegate 取舍:05-应用场景与进阶实践
- 性能与内存:上文的「不同传递方式的性能分析」与「行业场景选型」可作为架构评审时的检查项。
参考文献
- Apple: Using responders and the responder chain to handle events
- 设计模式:Delegate、Chain of Responsibility(责任链)、Observer(多监听者可结合 for 遍历)