🔄 两种限流方式对比
| 方式 | 目标 | 典型操作符 | 效果 |
|---|---|---|---|
| 流速控制 | 限制生产者速率 | conflate(), buffer() |
控制数据产生速度 |
| 处理频率控制 | 限制消费者处理频率 | debounce(), throttleLatest() |
控制数据处理速度 |
🎯 1. 流速控制(生产者侧)
这是您提到的场景:在收集端通过 delay 控制 。但这种方法不推荐,因为它阻塞了整个收集管道。
kotlin
// ❌ 不推荐:在 collect 中 delay 会阻塞整个流
flow {
for (i in 1..10) {
emit(i)
delay(10) // 生产者每10ms生产一个
}
}
.collect { value ->
delay(100) // ❌ 这样会阻塞整个流,导致缓冲区积压
println("消费: $value")
}
更好的生产者限流方式:
a) 在源头控制速率
kotlin
fun rateLimitedFlow(): Flow<Int> = flow {
for (i in 1..100) {
emit(i)
delay(100) // ⭐ 在源头控制:每100ms最多生产一个
}
}
// 消费者可以正常消费
rateLimitedFlow().collect { value ->
// 不需要额外 delay
println("消费: $value")
}
b) 使用 throttleLatest(Kotlin 1.7+)
kotlin
import kotlinx.coroutines.flow.*
suspend fun throttleLatestExample() {
flow {
repeat(50) { i ->
emit(i)
delay(50) // 每50ms生产一个
}
}
.throttleLatest(200) // ⭐ 每200ms取最新值,丢弃中间的
.collect { value ->
println("[${System.currentTimeMillis()}] 处理: $value")
// 输出频率被限制为每200ms最多一次
}
}
🕒 2. 处理频率控制(消费者侧)
这才是真正的**"限流",控制处理的频率**而不是阻塞管道。
a) debounce() - 防抖(等待稳定)
kotlin
// 场景:搜索框输入,用户停止输入300ms后才触发搜索
searchQueryFlow
.debounce(300) // ⭐ 300ms内没有新值才发出最后一个
.collect { query ->
performSearch(query)
}
// 示例:快速输入"hello"
// 输入: h(0ms) e(50ms) l(100ms) l(150ms) o(200ms)
// 输出: 只在 500ms 时收到 "hello"(最后o输入后300ms)
b) sample() - 采样(定期取样)
kotlin
// 场景:实时图表更新,每100ms采样一次
sensorFlow
.sample(100) // ⭐ 每100ms取一个样本(时间点最近的值)
.collect { data ->
updateChart(data) // 每100ms更新一次,不管传感器多快
}
// 更完整的示例:
fun samplingExample() {
val startTime = System.currentTimeMillis()
flow {
repeat(20) { i ->
emit(i)
delay(30) // 每30ms产生一个值
}
}
.sample(100) // 每100ms采样一次
.collect { value ->
val elapsed = System.currentTimeMillis() - startTime
println("[$elapsed ms] 采样值: $value")
// 输出:
// [100 ms] 采样值: 2
// [200 ms] 采样值: 5
// [300 ms] 采样值: 8
// [400 ms] 采样值: 11
}
}
c) 基于时间的自定义限流
kotlin
fun Flow<Int>.customThrottle(windowMillis: Long): Flow<Int> = flow {
var lastEmitTime = 0L
collect { value ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmitTime >= windowMillis) {
emit(value)
lastEmitTime = currentTime
}
// 否则丢弃这个值
}
}
// 使用
fastFlow
.customThrottle(100) // 每100ms最多处理一个
.collect { value ->
processValue(value)
}
📊 3. 综合实战:车载信号处理
在您的车载空调App中,不同信号需要不同的限流策略:
kotlin
class VehicleSignalProcessor {
// 1. 温度传感器数据(高频,只需定期采样)
fun processTemperature(sensorFlow: Flow<Float>): Flow<Float> {
return sensorFlow
.sample(1000) // 每秒采样一次(温度变化慢)
.conflate() // 合并中间值,取最新
}
// 2. 用户按键事件(需要防抖)
fun processButtonEvents(buttonFlow: Flow<ButtonEvent>): Flow<ButtonEvent> {
return buttonFlow
.debounce(50) // 50ms防抖,防止误触
.distinctUntilChanged() // 去重
}
// 3. CAN总线数据(需要时间窗口统计)
fun processCanMessages(canFlow: Flow<CanMessage>): Flow<List<CanMessage>> {
return canFlow
.buffer(capacity = 100) // 缓冲100条
.windowed(size = 10, timeMillis = 100) // ⭐ 每100ms的10条数据作为一个批次
.map { batch ->
// 批量处理CAN消息
batch.filter { it.isValid() }
}
}
// 4. 基于自定义时间窗口的限流
fun processWithTimeWindow(): Flow<Int> {
val flow = flow {
repeat(100) { i ->
emit(i)
delay((10..50).random().toLong()) // 随机间隔10-50ms
}
}
return flow
.customTimeWindow(windowSize = 5, timeMillis = 100) // 100ms内的5个值
.map { window ->
// 处理时间窗口内的数据
window.average().toInt()
}
}
}
// 自定义:时间窗口操作符
fun <T> Flow<T>.customTimeWindow(
windowSize: Int,
timeMillis: Long
): Flow<List<T>> = channelFlow {
val buffer = mutableListOf<T>()
var windowStartTime = System.currentTimeMillis()
collect { value ->
buffer.add(value)
val currentTime = System.currentTimeMillis()
val isWindowFull = buffer.size >= windowSize
val isTimeElapsed = currentTime - windowStartTime >= timeMillis
if (isWindowFull || isTimeElapsed) {
send(buffer.toList())
buffer.clear()
windowStartTime = currentTime
}
}
// 发送剩余数据
if (buffer.isNotEmpty()) {
send(buffer.toList())
}
}
🎮 4. 可视化的限流效果
让我们通过一个具体例子看不同限流策略的效果:
kotlin
suspend fun visualizeThrottling() {
val sourceFlow = flow {
repeat(30) { i ->
emit(i)
delay(20) // 每20ms一个值,持续600ms
}
}
println("=== 原始数据流 ===")
val startTime = System.currentTimeMillis()
sourceFlow.collect { value ->
val elapsed = System.currentTimeMillis() - startTime
println("[${elapsed}ms] 原始: $value")
}
println("\n=== 使用 sample(100ms) ===")
sourceFlow
.sample(100)
.collect { value ->
val elapsed = System.currentTimeMillis() - startTime
println("[${elapsed}ms] 采样: $value")
// 大约在 100ms, 200ms, 300ms... 输出
}
println("\n=== 使用 debounce(50ms) ===")
sourceFlow
.debounce(50)
.collect { value ->
val elapsed = System.currentTimeMillis() - startTime
println("[${elapsed}ms] 防抖: $value")
// 只会在流结束后输出最后一个值 29
}
}
输出效果对比:
=== 原始数据流 ===
[20ms] 原始: 0
[40ms] 原始: 1
[60ms] 原始: 2
... (每20ms一个)
=== 使用 sample(100ms) ===
[100ms] 采样: 4 ← 100ms时最近的值是第4个
[200ms] 采样: 9 ← 200ms时最近的值是第9个
[300ms] 采样: 14 ← 以此类推
=== 使用 debounce(50ms) ===
[650ms] 防抖: 29 ← 最后一个值发出后50ms才输出
🎯 实战选择指南
| 场景 | 推荐策略 | 代码示例 |
|---|---|---|
| UI输入防抖 | debounce(300) |
searchFlow.debounce(300) |
| 传感器数据 | sample(interval) |
sensorFlow.sample(100) |
| 实时图表 | 时间窗口平均 | .windowed(10, 1000).map { it.average() } |
| 网络请求队列 | 固定速率 | 使用 delay 在源头控制 |
| 车载CAN信号 | 批次处理 | .buffer().windowed(size, time) |
关键点:
debounce:等待操作结束(如用户停止输入)sample:定期取样(如定时刷新UI)- 时间窗口 :批量处理固定时间段内的数据
- 源头控制:在生产端控制速率(最根本的限流)
在您的车载空调App中,对于DDS信号:
- 控制指令 :用
debounce防止频繁发送 - 状态更新 :用
sample定期采样显示 - 日志记录:用时间窗口批量写入文件