防抖与限流
在处理高频触发的事件流(如用户输入、传感器采集或滚动回调)时,如果不加控制地处理每一个发射值,会导致 CPU 过载、无效的 UI 刷新或密集的网络请求。Flow 提供了一系列时间算子,允许开发者根据时间窗口对数据流进行精确的降频处理。
防抖:debounce
debounce(防抖)的核心逻辑是:只有当上游在指定的时间窗口内没有发射新值时,才发射最后一个值。如果新值在窗口期内到来,则计时器重置。
- 逻辑本质:寻求“稳定态”。
- 应用场景:搜索联想、输入校验、保存配置到本地。
kotlin
searchQueryFlow
// 用户停止打字 300ms 后才触发请求
.debounce(300L)
.flatMapLatest { query -> fetchResults(query) }
.collect { showUI(it) }kotlin
flow {
emit(1); delay(100)
emit(2); delay(500) // 停顿足够长
emit(3)
}
.debounce { value ->
// 可以为不同的值设置不同的防抖时间
if (value % 2 == 0) 200L else 400L
}
.collect { println(it) }采样:sample
sample(采样)按固定的时间周期提取流中的最新值。无论上游发射多快,每一个周期内最多只能下发一个值。
- 逻辑本质:强制“匀速化”。
- 应用场景:仪表盘 UI 刷新、高频传感器数据观测。
与 conflate 的区别
conflate 取决于消费者的处理速度,而 sample 取决于预设的时间周期。即使消费者处理很快,sample 依然会按照固定频率控制数据流。
kotlin
// 传感器每 1ms 发射一次数据
sensorFlow
.onEach { compute(it) }
.sample(100L) // 每 100ms 取一次最新状态更新 UI
.collect { updateDashboard(it) }进阶:自定义节流算子 (Throttle)
虽然 Kotlin 官方库目前未直接内置名为 throttleFirst 的操作符,但在 Android 开发中,“防连击”是一个极高频的需求。我们可以利用 flow 构建器快速实现。
throttleFirst 逻辑实现
接收第一个值并立即发射,随后在指定窗口期内忽略所有值。
kotlin
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
var lastEmissionTime = 0L
collect { value ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmissionTime > windowDuration) {
lastEmissionTime = currentTime
emit(value)
}
}
}
// 应用:防止按钮 500ms 内重复点击
buttonClickFlow
.throttleFirst(500L)
.collect { doSubmit() }核心开发准则
- 选择 debounce 的直觉:只要涉及“等待用户输入结束”或“消除抖动”的场景,首选
debounce。 - 选择 sample 的直觉:只要涉及“高频数据流中提取特征点”或“固定频率同步 UI”的场景,首选
sample。 - 不要在 debounce 中执行重型计算:
debounce本身涉及协程的定时挂起与取消。如果其下游处理太慢,依然会触发背压。建议配合flowOn将计算转移到后台。 - 注意内存开销:
debounce需要在内存中短暂持有未发射的元素。在处理超大数据块时,应留意内存占用。 - 合理组合 distinctUntilChanged:在
debounce或sample之后紧跟distinctUntilChanged是一个极佳的实践,可以进一步减少不必要的 UI 重绘。