一、Flow 的背压(Backpressure)
1)默认策略:
挂起传播(suspend)
-
Flow 是冷流 ,上游 emit、下游 collect 默认是顺序串行。
-
当下游慢时:上游的 emit() 会挂起等待下游消费完,再继续发下一条------这就是 Flow 天然的背压。
2)改变背压行为的常用算子
算子 | 行为 | 典型用途 |
---|---|---|
buffer(capacity) | 在上下游之间放缓冲区;满了再挂起 | "平滑"短时抖动;IO 与 UI 解耦 |
conflate() | 只保留最新,丢弃中间值 | UI 连续进度/位置,只关心最新 |
collectLatest { ... } | 新值来时取消旧值的处理 | 搜索建议、图片解码、渲染任务 |
debounce(t) | 静默期后发最后一个 | 文本输入防抖 |
sample(t) | 每 t 时间取一个最新值 | 高频来源的节流 |
zip vs combine | zip 配对等待;combine 来一个就合并最新 | 双源对齐 vs 实时混合 |
小结:不丢数据 用 buffer;要最新 用 conflate/collectLatest/sample/debounce;严格一一配对用 zip。
3)Hot Flow 的背压特性
-
StateFlow :天然合流/覆盖(只保留最新),不会"背压阻塞"上游。适合状态。
-
SharedFlow:可配置缓冲:
-
MutableSharedFlow(replay, extraBufferCapacity, onBufferOverflow = SUSPEND|DROP_OLDEST|DROP_LATEST)
-
用于多订阅者广播时的背压策略选择。
-
4)flowOn与背压
- flowOn(Dispatcher) 把其上的上游 移到指定线程,还会在边界放缓冲通道,用以解耦上下游速率,避免主线程被上游阻塞。
- 若需要更强控制,可再配合 buffer()。
二、Flow 的并发(Concurrency)
1)Flow 的默认执行模型
-
顺序执行:每个算子(map/filter/transform)在同一协程里一条一条处理。
-
这意味着:map { withContext(Dispatchers.Default){...} } 不会并行,只是换了线程。
2)让转换并发起来的两个核心手段
A.flatMapMerge(concurrency = N)
把"单值→子流"的转换并并发运行 N 份 ,最终合并成一个流:
scss
flowOf(url1, url2, url3, url4)
.flatMapMerge(concurrency = 4) { url ->
flow { emit(downloadAndDecode(url)) } // 每个子流可在 IO/Default 上工作
}
.flowOn(Dispatchers.IO) // 上游下载在 IO
.onEach { render(it) } // 下游在调用处上下文(通常 Main)
.launchIn(lifecycleScope)
B.channelFlow { ... }
在构建器里可以并发启动多个子协程,对同一 send() 通道推数据(安全、有序):
scss
channelFlow {
val urls = listOf(url1, url2, url3, url4)
val sem = Semaphore(permits = 4) // 自控并发度
urls.forEach { url ->
launch(Dispatchers.IO) {
sem.withPermit {
val bmp = downloadAndDecode(url)
send(bmp) // 推给下游
}
}
}
}.onEach { render(it) }.launchIn(lifecycleScope)
对比:flatMapMerge 简洁、足够常用;channelFlow 更灵活(并发结构、取消、超时、择优返回等)。
3)其它并发相关算子
- mapLatest {} :新值来就取消旧任务(常用于 UI 渲染、搜索)。
- merge(flowA, flowB, ...) :把多个流并发合并。
- combine(flowA, flowB) :任何一端有新值就合并最新快照(不是配对)。
三、典型落地范式(可直接套用)
场景 1:IO → 解码 → UI 渲染(上下游解耦 + 背压可控)
scss
imageIdFlow
.map { id -> loadBytes(id) } // IO
.flowOn(Dispatchers.IO)
.buffer(capacity = 8) // 抗抖动,避免 UI 抽风卡顿
.mapLatest { bytes -> decodeBitmap(bytes) } // 新图来就取消旧解码
.flowOn(Dispatchers.Default) // 解码放 CPU 线程池
.onEach { bmp -> imageView.setImageBitmap(bmp) } // 主线程渲染
.launchIn(lifecycleScope)
- 背压策略:缓冲 8 张,解码用 mapLatest 丢弃过时任务,保证 UI 只渲染最新。
场景 2:列表批量任务并发(受限 N 路)
scss
flow { mediaIds.forEach { emit(it) } }
.flatMapMerge(concurrency = 4) { id ->
flow {
val meta = queryMediaStore(id) // IO
val thumb = decodeThumb(meta) // CPU
emit(thumb)
}.flowOn(Dispatchers.IO)
}
.onEach { submitThumb(it) }
.launchIn(lifecycleScope)
- flatMapMerge(4) 控制并发;每个子流内再 flowOn(IO),CPU/IO 可再细分。
场景 3:搜索输入(防抖 + 取消旧请求)
scss
textChangesFlow
.debounce(300)
.distinctUntilChanged()
.mapLatest { q -> api.search(q) } // 新词来了取消上一个网络请求
.catch { emit(emptyList()) }
.onEach(::renderResults)
.launchIn(lifecycleScope)
场景 4:全局状态(只要最新,不做背压)
kotlin
val _state = MutableStateFlow(UiState())
val state: StateFlow<UiState> = _state
// 频繁 setValue 也不会"压爆",下游永远拿到**最新** UiState
场景 5:广播事件(可配置溢出策略)
ini
val events = MutableSharedFlow<Event>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST // 或 SUSPEND / DROP_LATEST
)
四、调度与上下游("上游/下游"分界)
-
同协程、顺序执行是默认:一个操作卡住,后面都等。
-
flowOn(X) :把其上的链路切到调度器 X,并在此处放缓冲;其下游仍在调用方上下文(比如 Main)。
-
buffer() :可以在任意位置放;常见组合是 flowOn(IO).buffer().mapLatest{...}.flowOn(Default)。
口诀:先 flowOn 再 buffer 再"重活" ;UI 渲染在 Main,重活各归其位。
五、错误处理与取消
- collectLatest/mapLatest 是通过取消旧协程来"丢弃"旧任务,注意释放资源(try {..} finally {..})。
- catch {} 只能捕获其上游异常;要捕获下游请包在 runCatching {} 或在外层 try。
- 对于 channelFlow/多并发子协程,建议用 SupervisorScope 防止一条失败把全局取消。
六、常见误区(避坑)
-
"withContext 就并行了" ❌
不是。并发要用 flatMapMerge / merge / channelFlow。
-
在 UI 上游做重活 ❌
记得 flowOn(IO/Default),并留出 buffer。
-
到处用无限 buffer ❌
易 OOM。容量要结合业务峰值测出来,或改用 conflate / sample。
-
把事件当状态用 ❌
一次性事件用 SharedFlow;状态用 StateFlow(天然合并最新)。
-
热流重复做重活 ❌
多个收集者共享上游:用 shareIn(stateIn),并合理 replay/WhileSubscribed。
七、迷你基准模板(压测背压与并发)
scss
val t0 = SystemClock.elapsedRealtime()
(1..1000).asFlow()
.onEach { delay(1) } // 模拟上游 1ms/条
.buffer(64) // 改改容量看总耗时与内存
.flatMapMerge(concurrency = 8) { i ->
flow {
// 模拟 CPU 重任务 2ms
withContext(Dispatchers.Default) { busyLoop(2) }
emit(i)
}
}
.onCompletion { println("cost=${SystemClock.elapsedRealtime()-t0}ms") }
.launchIn(GlobalScope) // 仅演示,实际请用生命周期域
八、快速选型清单
- 我不想丢数据:buffer(n) → 不够再扩 → 必要时 backpressure(挂起)
- 我只要最新:conflate() / collectLatest {} / sample() / StateFlow
- 我要 N 路并发:flatMapMerge(concurrency=N) / channelFlow + Semaphore
- 多来源合并:merge()(并发) / combine()(最新) / zip()(配对)
- 线程/调度:flowOn(IO/Default) + 必要 buffer(),UI 渲染在 Main