三分钟搞懂 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 会替你处理其余的一切。

相关推荐
柯南二号3 小时前
【大前端】【Android】把 Activity 重构成 MVVM 的对比示例
android·状态模式
某空m3 小时前
【Android】Glide的缓存机制
android·缓存·glide
某空m3 小时前
【Android】Glide的使用
android·glide
QING6183 小时前
Jetpack Compose 中的 ViewModel 作用域管理 —— 新手指南
android·kotlin·android jetpack
鹏多多4 小时前
flutter-使用EventBus实现组件间数据通信
android·前端·flutter
ShayneLee84 小时前
Nginx修改请求头响应头
android·运维·nginx
廋到被风吹走4 小时前
【数据库】【MySQL】高可用与扩展方案深度解析
android·数据库·mysql
恋猫de小郭4 小时前
Flutter 官方正式解决 WebView 在 iOS 26 上有点击问题
android·前端·flutter
CaspianSea8 小时前
编译Android 16 TV模拟器(一)
android