三分钟搞懂 Kotlin Flow 中的背压

你有没有遇到过这样的情况:数据源发送得太快,你的应用根本处理不过来,结果要么卡顿要么崩溃?

针对这种情况,Kotlin Flow 提供了一些内置的方法,让你的生产者和消费者能步调一致。在这篇文章里,我们会聊聊:

  1. 背压(Backpressure)是什么意思?
  2. 默认的"等一等"模式是怎么工作的?
  3. 什么时候用 buffer() 加个小队列?
  4. conflate() 怎么跳过旧数据?
  5. collectLatest { } 为什么会在新数据到来时停止旧任务?
  6. 如何根据自己的情况选择合适的方案?

背压(Backpressure)是什么

背压 就是确保快速的数据发送方不会把慢速的接收方给压垮。要是没有这玩意儿,内存里可能会堆满未处理的数据,或者花时间处理已经过时的信息。

举个简单的例子。

我们知道,Android 屏幕的刷新率是一个固定值,60 fps,90 fps,好一点的收集 120 fps,想象一下我们写的 UI 从流那里收集数据,如果这个数据发送发每秒钟超过了 200 次,那么其实其中有好几十次是没用的,因为这些数据根本不会渲染在屏幕上。

实际上接收方的数据,只要保持 60 次/秒就很够用了。

此时,你就需要背压!

背压的好处有:

  • 控制内存使用
  • 避免做无用功
  • 让应用性能更稳定

默认的"等一等"模式

默认情况下,当你这么写代码时:

Kotlin 复制代码
suspend fun main() {
    flow {
        (1..3).forEach {
            timeLog("Send $it")
            emit(it)
            delay(100)            // 快速发送方
        }
    }.collect { value ->
        timeLog("Processing $value")
        delay(300)             // 慢速处理方
    }
}

// output:
// 20:22:19.647 Send 1
// 20:22:19.701 Processing 1
// 20:22:20.142 Send 2
// 20:22:20.143 Processing 2
// 20:22:20.564 Send 3
// 20:22:20.564 Processing 3

发送方emit)会暂停,直到 处理方collect)处理完上一个值。这里没有队列,每个值都是一次发送、一次处理。

buffer:加个小队列

如果你想让发送方稍微提前一点,可以这样:

Kotlin 复制代码
flow {
//...
}
.buffer(capacity = 2)
.collect { value ->
//...    
}

// output:
// 20:27:03.368 Send 1
// 20:27:03.418 Processing 1
// 20:27:03.529 Send 2
// 20:27:03.639 Send 3
// 20:27:03.732 Processing 2
// 20:27:04.470 Processing 3

整体时间变快了!

  • 现在发送方最多可以把 2 个元素放入一个小队列中。
  • 一旦队列满了,它就会再次暂停。

这就给了你一个 有限队列:你仍然会处理每一个元素,但可以平滑处理速度上的突发波动。

conflate:跳过旧数据

当你只关心最新数据时(比如更新进度条),可以这样写:

Kotlin 复制代码
flow { ... }
.conflate()
.collect { value -> ... }

// output:
// 20:30:45.745 Send 1
// 20:30:45.813 Processing 1
// 20:30:45.930 Send 2
// 20:30:46.039 Send 3
// 20:30:46.133 Processing 3
  • 如果处理方正忙,只有 最新的 未处理项会被保留。
  • 旧的项会被丢弃,所以你永远不会处理过时的更新。

注意conflate() 不会停止当前正在进行的工作;它只是在下一次读取时跳过旧值。

collectLatest:取消旧任务

如果你希望在新数据到来时立即取消正在进行的任务,可以这样:

Kotlin 复制代码
suspend fun main() {
    flow {
        (1..3).forEach {
            timeLog("Send $it")
            emit(it)
            delay(100)            
        }
    }.collectLatest { value ->
        timeLog("Start process $value")
        delay(300)             
        timeLog("Complete process $value")
    }
}

// output:
// 09:31:32.919 Send 1
// 09:31:32.979 Start process 1
// 09:31:33.090 Send 2
// 09:31:33.093 Start process 2
// 09:31:33.195 Send 3
// 09:31:33.195 Start process 3
// 09:31:33.498 Complete process 3

你会发现,只有 3 完成了任务。

  • 每当有新的 emit 发生时,正在处理前一个值的代码块会立刻被丢弃。
  • 只有当发送方停止发送后,你才会完成对 最后一个 值的处理。

这种模式非常适合 "边输入边搜索" 的场景------用户继续输入时,你可以立即放弃之前的搜索请求。

抉择指南

  • collect
    作用 :发送方和处理方逐个等待,一一对应
    适用场景 :你需要按顺序处理 每一个
  • buffer
    作用 :创建一个容量为 n 的小队列;不会丢弃任何元素
    适用场景:你想有一点缓冲能力,但仍需处理所有元素
  • conflate
    作用 :如果处理方正忙,只保留 最新的 元素
    适用场景:你需要最新数据,但仍希望完成当前正在处理的任务
  • collectLatest
    作用 :新数据一到就立即取消正在进行的任务
    适用场景 :只有 最新结果 有意义;其他一切都可以立刻丢弃

下次当你觉得 Flow 太快或太慢时,问问自己:

  1. 我是否需要处理每一个值?
  2. 一个小队列会有帮助吗?
  3. 是否只有最新数据才重要?
  4. 新数据到来时,我是否应该取消旧任务?

选择最简单的、能满足需求的选项,Kotlin Flow 会替你处理其余的一切。

相关推荐
_李小白6 小时前
【Android FrameWork】第四十九天:SystemUI
android
Mr -老鬼6 小时前
移动端跨平台适配技术框架:从发展到展望
android·ios·小程序·uni-app
城东米粉儿6 小时前
compose measurePoliy 笔记
android
城东米粉儿6 小时前
Compose 延迟列表
android
GoldenPlayer6 小时前
SOLID原则-Software Develop
android
GoldenPlayer6 小时前
Android文件管理系统
android
冬奇Lab6 小时前
【Kotlin系列02】变量与数据类型:从val/var到空安全的第一课
android·kotlin·编程语言
alonewolf_996 小时前
深入理解MySQL事务与锁机制:从原理到实践
android·数据库·mysql
深海呐6 小时前
Android WebView吊起软键盘遮挡输入框的问题解决
android·webview·android 键盘遮挡·webview键盘遮挡
摘星编程6 小时前
RAG的下一站:检索增强生成如何重塑企业知识中枢?
android·人工智能