在响应式编程中,Backpressure(背压) 是一个至关重要的概念。简单来说,它描述的是:当数据的产生速度(Publisher)超过了数据的处理速度(Subscriber)时,系统如何进行协调,防止内存溢出或程序崩溃。
想象一个漏斗:如果上方倒水的速度远快于下方细管流出的速度,漏斗就会溢出。Backpressure 就是一套机制,让下方的细管能告诉上方的壶:“慢点倒,我处理不过来了。”
1. Combine 处理 Backpressure 的核心:拉取模型(Pull Model)
与许多早期的响应式框架(如传统的 Observable “推”模型)不同,Combine 建立在 基于需求的拉取模型(Demand-based Pull Model) 之上。
核心机制:Demand
在 Combine 的订阅生命周期中,Subscriber 并不是被动地接收所有数据,而是主动告诉 Publisher 它能处理多少数据。
-
建立连接:Subscriber 订阅 Publisher。
-
发送订阅对象:Publisher 向 Subscriber 发送一个
Subscription对象。 -
请求数据(核心步骤) :Subscriber 通过
Subscription调用request(_ demand: Subscribers.Demand)。Demand.unlimited:我要所有数据,你能发多快就发多快。Demand.max(k):我目前只能再处理 个值。Demand.none:我现在不想要数据。
-
按需发送:Publisher 只有在收到
Demand后,才会发送相应数量的值。
2. 动态调整需求
Backpressure 不是一次性的握手,而是一个动态过程。
当 Subscriber 的 receive(_ input:) 函数被触发时,它会返回一个新的 Demand 对象。这个返回值代表**“新增的需求量”**:
- 如果返回
.max(1),意味着“每收到 1 个值,我允许你再多发 1 个”。 - 如果返回
.none,需求量保持不变。 - 注意:需求量是累加的。一旦 Subscriber 请求了
.max(10),Publisher 就获得了发送 10 个值的权限,这个权限无法被撤回。
3. 处理 Backpressure 的常用策略
虽然 Combine 的底层协议支持 Backpressure,但许多内置的 Publisher(如 Timer 或 NotificationCenter)是强制推送的(它们无法停下来等你)。针对这种无法减速的源头,Combine 提供了几种缓冲策略:
A. 缓冲(Buffering)
使用 .buffer 操作符。你可以指定一个缓冲区大小和溢出策略(如丢弃最旧的值或直接报错)。
Swift
publisher
.buffer(size: 100, prefetch: .byRequest, whenFull: .dropOldest)
B. 节流与防抖(Throttling & Debouncing)
- Debounce:在一段时间内没有新信号产生时才发送最后一条(常用于搜索框输入)。
- Throttle:在固定时间窗口内只发送一条数据(常用于按钮高频点击)。
C. 丢弃(Dropping)
使用 .dropFirst() 或类似操作符直接忽略掉某些不重要的数据点。
4. 防御式编程:为什么你的自定义 Subscriber 不动了?
如果你自己实现 Subscriber 协议,最容易犯的错误就是忘记请求数据。
- 陷阱:如果在
receive(subscription:)中没有调用subscription.request(.max(n)),Publisher 将永远不会发送任何数据。 - 结果:你的 pipeline 看起来是通的,但没有任何数据流过。
5. Backpressure 的局限性
虽然 Backpressure 解决了流量控制,但它并不是万能药:
- 物理限制:如果数据源是硬件传感器(如陀螺仪),它无法因为你的 Demand 而停止产生物理数据。此时 Backpressure 实际上转化为了丢弃策略。
- 逻辑复杂性:过度精细的 Demand 管理会让代码变得难以维护。
总结
Backpressure 保证了系统的稳定性。 在 Combine 中,它将“推”模型转变为“拉”模型,确保了 Subscriber 始终掌握着处理节奏的主动权。