详解flowOn 与背压

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"来布置运算。

相关推荐
南北是北北2 小时前
让转换并发起来的两个核心手段:flatMapMerge(concurrency = N)和channelFlow { ... }
面试
9号达人2 小时前
Java 14 新特性详解与实践
java·后端·面试
青鱼入云4 小时前
【面试场景题】支付&金融系统与普通业务系统的一些技术和架构上的区别
面试·金融·架构
掘金安东尼4 小时前
黑客劫持:周下载量超20+亿的NPM包被攻击
前端·javascript·面试
在未来等你9 小时前
Elasticsearch面试精讲 Day 17:查询性能调优实践
大数据·分布式·elasticsearch·搜索引擎·面试
围巾哥萧尘13 小时前
美式审美的商务头像照🧣
面试
不要再敲了17 小时前
JavaScript与jQuery:从入门到面试的完整指南
javascript·面试·jquery
月阳羊19 小时前
【硬件-笔试面试题-95】硬件/电子工程师,笔试面试题(知识点:RC电路中的时间常数)
java·经验分享·单片机·嵌入式硬件·面试
yinke小琪19 小时前
说说hashCode() 和 equals() 之间的关系
java·后端·面试