Skip to content

防抖与限流

源:Flow buffering

在处理高频触发的事件流(如用户输入、传感器采集或滚动回调)时,如果不加控制地处理每一个发射值,会导致 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() }

核心开发准则

  1. 选择 debounce 的直觉:只要涉及“等待用户输入结束”或“消除抖动”的场景,首选 debounce
  2. 选择 sample 的直觉:只要涉及“高频数据流中提取特征点”或“固定频率同步 UI”的场景,首选 sample
  3. 不要在 debounce 中执行重型计算debounce 本身涉及协程的定时挂起与取消。如果其下游处理太慢,依然会触发背压。建议配合 flowOn 将计算转移到后台。
  4. 注意内存开销debounce 需要在内存中短暂持有未发射的元素。在处理超大数据块时,应留意内存占用。
  5. 合理组合 distinctUntilChanged:在 debouncesample 之后紧跟 distinctUntilChanged 是一个极佳的实践,可以进一步减少不必要的 UI 重绘。