8、Flow的线程切换与背压:flowOn切换上下文、buffer、conflate、collectLatest处理背压问题

好,咱们今天来聊聊 Flow 里两个非常核心的实操话题——线程切换背压

说实话,我早期用 RxJava 的时候,背压就是个让人头疼的东西。到了 Kotlin Flow,Google 的设计思路变了——它默认是冷流、顺序执行,但实际项目中你不可能永远在同一个线程里跑。所以,flowOnbufferconflatecollectLatest 这几个操作符,就成了我们手里的利器。

下面我一个个拆开讲,结合我踩过的坑,给你说明白。

8.1 flowOn:切换上游的调度器

flowOn 的作用很简单——改变 Flow 上游(即数据发射端)所在的协程上下文。下游的 collect 默认跑在调用 collect 的协程里,而 flowOn 只影响它前面的操作。

来看个例子:

flow {
    for (i in 1..5) {
        delay(100) // 模拟耗时操作
        emit(i)
    }
}
.flowOn(Dispatchers.IO) // 上游切到 IO 线程
.collect { value ->
    // 这里跑在调用 collect 的线程(比如 Main)
    Log.d("Flow", "收到: $value")
}

我个人习惯把 flowOn 放在靠近 flow {} 构建器的地方,这样一眼就能看出数据是从哪个线程发射的。你想想看,如果上游有多个操作符,flowOn 只影响它之前的那一段。

关键点flowOn 可以多次调用,每次都会切换上游的上下文。但要注意,频繁切换线程会带来额外的开销。

8.2 buffer:引入缓冲区,让上下游并行跑

默认情况下,Flow 是顺序执行的——上游发射一个,下游处理一个,处理完再发射下一个。如果下游处理很慢,上游就得等着。这就是典型的背压场景。

buffer 操作符会在上下游之间加一个缓冲区。上游可以继续发射数据到缓冲区,下游从缓冲区取数据。这样上下游就能并行执行了。

flow {
    for (i in 1..10) {
        delay(100) // 每 100ms 发射一个
        emit(i)
    }
}
.buffer(10) // 缓冲区容量为 10
.collect { value ->
    delay(300) // 下游处理需要 300ms
    println("处理: $value")
}

没有 buffer 时,总耗时是 100ms + 300ms = 400ms 每个元素。加了 buffer 后,上游可以提前发射,下游慢慢消费,整体耗时大幅降低。

避坑指南:我曾经在项目中把 buffer 设得特别大(比如 1000),结果内存飙升。缓冲区不是越大越好,要根据你的数据量和处理速度来定。一般 10~50 就够用了。

8.3 conflate:只保留最新的数据

有些场景下,你并不关心中间值,只想要最新的。比如 UI 更新——用户快速滑动列表,你只需要展示最终位置,中间跳过的那些帧丢了也无所谓。

conflate 就是干这个的。它本质上是一个容量为 1 的缓冲区,但有个特殊行为:如果下游还在处理上一个数据,上游新发射的数据会直接覆盖缓冲区里的旧数据

flow {
    for (i in 1..10) {
        delay(50)
        emit(i)
    }
}
.conflate()
.collect { value ->
    delay(200)
    println("最终处理: $value")
}

运行后你会发现,中间很多值被跳过了,只处理了最后一个。嗯,这里要注意:conflate 不会丢数据,它只是丢弃了那些「来不及处理」的旧数据。

注意conflate 适用于「结果覆盖型」场景,比如进度更新、位置刷新。如果你需要处理每一个值(比如日志记录),千万别用 conflate。

8.4 collectLatest:取消旧任务,执行最新

collectLatestconflate 有点像,但行为完全不同。

collectLatest 的逻辑是:当下游开始处理一个值时,如果上游又发射了新值,那么当前正在执行的下游处理会被取消,然后立即开始处理新值。

flow {
    for (i in 1..5) {
        delay(100)
        emit(i)
    }
}
.collectLatest { value ->
    delay(300) // 模拟耗时处理
    println("处理完成: $value")
}

运行结果:只有最后一个值被完整处理了。因为每次新值到来时,上一个 delay(300) 还没跑完就被取消了。

我个人觉得 collectLatest 最适合的场景是搜索联想——用户每输入一个字符,就发起一次网络请求。如果前一次请求还没回来,用户又输入了新字符,那前一次请求的结果就没意义了,直接取消掉,只处理最新的。

对比总结

  • buffer:并行处理,不丢数据,但可能积压。
  • conflate:只保留最新,丢弃旧数据。
  • collectLatest:取消旧任务,执行最新任务。

8.5 知识体系图:Flow 线程切换与背压

下面这张图帮你理清这几个操作符的关系和适用场景:

Flow 线程切换与背压处理 上游发射端 flowOn 切换线程上下文 背压处理策略 buffer conflate collectLatest 下游收集端 操作符对比: • buffer:并行处理,不丢数据,适合下游处理慢但需要完整数据的场景 • conflate:只保留最新,丢弃旧数据,适合进度/位置更新 • collectLatest:取消旧任务执行最新,适合搜索联想/频繁触发场景

8.6 实际项目中的组合用法

真实项目里,这几个操作符经常组合使用。我举个例子:

// 网络请求 + UI 更新
flow {
    // 从数据库或网络获取数据列表
    val list = api.fetchDataList()
    emit(list)
}
.flowOn(Dispatchers.IO) // 网络请求在 IO 线程
.buffer(5)              // 缓冲区,防止下游卡住上游
.conflate()             // 如果 UI 更新慢,只保留最新数据
.flowOn(Dispatchers.Main) // 下游 collect 在 Main 线程
.collect { list ->
    adapter.submitList(list)
}

你看,这里用了两次 flowOn。第一次把上游切到 IO,第二次又把下游切回 Main。中间用 bufferconflate 处理背压。这种写法在 MVVM 的 Repository 层很常见。

我的经验:如果你不确定用哪个,先问自己两个问题——「我需要处理每一个值吗?」和「下游处理速度比上游慢多少?」。需要全部处理就用 buffer,不需要就用 conflate 或 collectLatest。

8.7 避坑指南

  • 不要滥用 flowOn:每次切换线程都有开销。我见过有人每个操作符后面都加 flowOn,结果性能反而更差。
  • buffer 的容量要合理:设太大容易 OOM,设太小又起不到并行效果。建议根据数据峰值来定。
  • collectLatest 的取消是协程级别的:如果下游处理里有不可取消的操作(比如文件写入),collectLatest 可能不会生效。
  • conflate 和 collectLatest 不要混用:它们解决的是类似的问题,但机制不同。混用会导致行为难以预测。

好了,关于 Flow 的线程切换和背压处理,就聊到这儿。这几个操作符用好了,你的 Flow 代码会既高效又优雅。


公众号:蓝海资料掘金营,微信 deep3321