第3章 Flow 创建与基础消费:flow 构建器、collect 操作符、默认背压行为
3.1 核心原理:冷流与挂起驱动的背压
Kotlin Flow 是一种冷数据流,即生产者(上游 Flow)只有在被终端操作符消费时才开始发射数据。其默认背压行为不依赖缓冲区或丢弃策略,而是通过协程挂起机制自然实现:collect 每处理一个值,上游 emit 必须等待该值被完全消费后才能发射下一个值。这种 “推拉结合” 的模式保证了数据不会在消费者未准备好时堆积,从而本质上避免了内存溢出或 CPU 空转。
关键理解:Flow 的默认背压不是“控制速率”,而是“等一等生产者”。每次 emit() 调用都会挂起当前协程,直到 collect 中的 lambda 执行完毕,再恢复发射下一个元素。这相当于生产者和消费者之间有一个隐式的一对一同步通道。
3.2 实现流程与 API 签名
创建并消费一个 Flow 通常分三步:
- 构建:使用
flow { ... }构建器定义发射序列。 - 中间操作(可选):应用 map、filter 等操作符转换数据。
- 终端消费:调用
.collect { ... }开始拉取数据。
关键 API 签名
| API | 类型 | 说明 |
|---|---|---|
fun | 构建器 | 在协程上下文中调用 emit() 发射数据 |
suspend fun Flow | 终端操作符 | 消费每个元素,挂起直到所有元素被处理 |
suspend fun FlowCollector | 发射函数 | 向上游 FlowCollector 发出值,挂起等待消费者处理 |
3.3 代码示例:基础消费与默认背压行为
以下代码演示了 flow { ... } 和 collect 的典型用法。注意 emit 与 collect 之间的挂起关系。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val flow = flow {
println("Flow 开始发射")
for (i in 1..3) {
delay(100) // 模拟异步工作
println("emit: $i")
emit(i)
}
println("Flow 发射结束")
}
println("开始 collect")
flow.collect { value ->
delay(200) // 模拟较慢的消费者
println("collect: $value")
}
println("collect 完成")
}
输出顺序验证背压行为:
开始 collect
Flow 开始发射
emit: 1
collect: 1
emit: 2
collect: 2
emit: 3
collect: 3
Flow 发射结束
collect 完成
分析:消费者
collect 处理每个值需要 200ms,而生产者每次发射前只等待 100ms。尽管如此,由于默认背压的挂起机制,生产者必须等待消费者完成 collect: 1 后才能发射第 2 个元素。整个流程被拉长到约 900ms,没有出现数据堆积或背压溢出。
3.4 默认背压行为结构图
下图展示了 flow 构建器与 collect 之间的交互过程,强调挂起点和背压传递关系。
3.5 常见错误与避坑
- 忽略挂起特性导致顺序假设错误:误以为
emit和collect是异步并行的。实际上它们是严格串行的,emit后的代码要等collect执行完毕才能继续。 - 在
flow构建器中直接使用阻塞 API:例如调用Thread.sleep()而非delay(),会阻塞协程线程,破坏流程控制。所有耗时操作应使用挂起函数。 - 忘记调用
collect导致 flow 不执行:Flow 是冷流,若不调用终端操作符,构建器内的代码永不会执行。调试时常见此问题。
性能陷阱:默认背压虽然安全,但如果消费者处理极快(< 1μs)而生产者发射密集(如循环无延迟),会导致消费者每步等待时间极短,但整体仍存在协程上下文切换开销。对于海量数据的批处理场景,考虑引入
buffer() 操作符以合并发射批次,减少挂起次数。但本章默认行为下不应随意加 buffer 以免混淆基础概念。
3.6 调试与日志技巧
在生产代码中观察背压行为最简单的工具是在 emit 和 collect 前后添加协程上下文日志:
flow {
println("[${coroutineContext}] 准备发射 $value")
emit(value)
println("[${coroutineContext}] 发射完成")
}.collect {
println("[${coroutineContext}] 消费 $it")
// 处理
}
利用 coroutineContext 打印的线程名称和 Job 状态,可以直观看到 emit 与 collect 在同一协程上下文中的交替执行顺序。
3.7 稳定性与默认背压的可靠性
由于默认背压不依赖缓冲区,不产生数据丢失或溢出,因此是构建可靠流式处理管道的基础。即使消费者偶尔慢速(如网络 I/O)、生产者突发高速,系统也不会崩溃,仅表现为总处理时间延长。这一特性使得 flow{…}.collect{…} 可安全用于关键路径,如传感器数据采集、文件逐行读取等场景。
| 特性 | 默认背压(挂起式) | 缓冲区背压(如 buffer, conflate) |
|---|---|---|
| 数据丢失风险 | 无(一对一交付) | 有(丢弃策略) |
| 内存占用 | 低(单元素) | 高(缓冲区大小) |
| 适用场景 | 可靠处理、顺序依赖 | 高吞吐、可容忍少量丢失 |
在进入更高级的背压控制操作符之前,深刻理解默认的挂起驱动背压是避免后续混淆的根基。