1)flowOn的本质:切线程 + 建边界
-
语义 :flowOn(dispatcher) 会把它上游 (到前一个 flowOn 或源头之间)的运算,放到指定 dispatcher 上执行;下游仍在收集者所在的上下文(通常是 Main)。
-
实现 :在该位置插入一个缓冲通道(Channel)与一个协程边界(上游与下游分开跑)。
-
效果 :即便上游很慢/很重,也不会长时间卡住下游线程 (比如 Main);当缓冲填满 时,上游才会被挂起 ------这就是背压。
对比:在 flow {} 里用 withContext(IO) 是违规 (会触发 "Flow invariant is violated"),正确做法是外置 flowOn(Dispatchers.IO) 。
简图:
scss
[ upstream ops ] --(buffer, ~64)--> [ downstream ops / collector(Main) ]
^ flowOn(IO) (Main/Default/...)
2) 背压细节与默认缓冲
- flowOn 自带一个有界缓冲(等价于 buffer(Channel.BUFFERED),常见容量≈64)。
- 背压触发 :当队列满了,上游的 emit/上游挂起点会被挂起,直到下游消费。
- 不丢数据 :默认策略是 SUSPEND(满就挂起,不丢、不覆盖)。
- 想要改变容量或策略(如 DROP_OLDEST),需要额外显式加 buffer(capacity, onBufferOverflow)。
3) 放置与调优:flowOn和buffer()搭配
3.1 单边界常用形态
scss
flow { /* IO/网络/磁盘 */ }
.map { parse() } // 希望也在 IO?放到 flowOn 之前
.flowOn(Dispatchers.IO) // ↑ 以上都在 IO,边界+缓冲
.onEach { renderShallow() } // ↓ 以下在收集者上下文(常是 Main)
.collect()
3.2 需要更"深"的队列
flowOn 的默认缓冲不够时,在它之后 再加一段缓冲(相当于加深 IO→Main 之间的排队池):
scss
upstream
.flowOn(Dispatchers.IO) // 自带 ~64
.buffer(256) // 再加深一层,降低上下游抖动耦合
.collect { ... }
3.3 多段流水线分仓(IO / CPU / Main)
用多个 flowOn 把不同类型的工作分区,并在区间之间按需 buffer():
scss
source()
.map { fetchFromDiskOrNet(it) } // IO 重
.flowOn(Dispatchers.IO) // ← 区间#1:IO
.buffer(128) // IO→CPU 之间加深缓冲
.map { heavyCpuDecode(it) } // CPU 重
.flowOn(Dispatchers.Default) // ← 区间#2:CPU
.buffer(64) // CPU→UI 之间加个小队列
.onEach { lightUiBind(it) } // UI 轻
.collect() // ← 区间#3:Main(收集者上下文)
口诀:谁要跑在哪个线程,就把它写在该 flowOn(X) 的上游**。**
4)buffer()放在flowOn的前vs后有何区别?
-
前 :缓冲仍然属于上游区 (已切过去的 dispatcher 上),适合在同一 dispatcher 内切开两段慢操作。
-
后 :缓冲位于区间之间 (跨 dispatcher 的边界后),更像"跨线程的排队池"。
实操建议:
- 只想加大 IO→Main 的队列 :放在 flowOn(IO) 后面。
- 想把同在 IO 的两步重活切开:在那两步中间直接 buffer(n)(再配 flowOn(IO))。
5) 与其它操作符的协作
- conflate() :只保"最新",适合 UI 只关心最新状态的段落(放在昂贵渲染前)。
- debounce()/sample() :按时间降频,减少下游重算/重绘频率。
- collectLatest / mapLatest :新值取消旧处理,对重计算/渲染很有效(尤其放在 flowOn 边界之后)。
- flatMapMerge(concurrency=n) :并行拉取/处理上游;flowOn 控线程、flatMapMerge 控并行度,二者可配合。
6) 热流(StateFlow/SharedFlow)注意点
-
flowOn 只影响它上游"在这条链上"的运算。
对于原本在别处 emit 的热流,flowOn 不会改变 那个 emit 发生的线程;它只改变你在热流之上的变换跑在哪个 dispatcher。
-
想改变生产端 线程,请在生产处 决定(例如在 ViewModel/Repository 内部切换上下文),或把重活放到你这条消费链路里,再用 flowOn 控制。
7) 错误/取消与上下文
- 上游在 flowOn 区间的异常会沿流向下 传播并取消下游;想就地兜底,catch {} 放在进入 flowOn 之前的链路上。
- 取消是协作式:CPU 密集段请添加挂起点或 yield()/isActive 检查,否则"取消不及时"。
8) 直观对比小实验
无 flowOn:主线程被顶住
scss
flow {
repeat(100) { i ->
Thread.sleep(30) // 模拟重 IO(错误示范)
emit(i)
}
}.onEach { delay(16) } // UI 绘制 60 FPS
.collect { /* UI 线程抖/卡 */ }
加上 flowOn(IO) + 深缓冲:UI 顺滑
scss
flow {
repeat(100) { i ->
delay(30) // 正确:挂起模拟 IO
emit(i)
}
}
.flowOn(Dispatchers.IO) // IO 区
.buffer(256) // IO→Main 深队列,削峰填谷
.onEach { delay(16) } // UI 区(收集者上下文)
.collect { /* 轻量渲染 */ }
9) 实战清单(拿去套)
- 磁盘/网络 + 解析 都很重:把解析也放到 flowOn(IO) 上游 ,必要时IO→CPU再分区。
- UI 抖动:flowOn 之后加 buffer(n),再配 conflate() 或 sample(16ms)。
- 峰值很狠但可丢中间:buffer(n, DROP_OLDEST)(或直接 conflate())。
- 必须逐条处理 :保持默认 SUSPEND,宁挂起不上游。
- 多段并行:flatMapMerge(concurrency=k) + 分区 flowOn,注意总并行度与 CPU/IO 配额。
一句话总结
flowOn 用"切线程 + 隐式缓冲边界"把快/慢段解耦,是 Flow 管背压的核心工具。
想进一步控队列与语义,就在边界前后 有意识地插 buffer()/conflate()/debounce(),并按"谁在谁上游,谁就跑在哪个 dispatcher"来布置运算。