节流(Throttle) 是控制事件频率的重要操作符,它确保在指定时间间隔内最多只处理一次事件。与防抖不同,节流是定期执行,而不是等待稳定。
一、节流的基本概念
1. 节流 vs 防抖
text
防抖 (Debounce):
输入: A----B----------C---D--------E
输出: B D E
特点: 等待一段时间,没有新事件才执行
节流 (Throttle):
输入: A-B---C-D---E-F---G
输出: A C E G
特点: 固定时间内只执行一次
2. 节流类型
- throttleFirst:时间窗口内的第一个事件
- throttleLast:时间窗口内的最后一个事件
- sample:定期采样最新值
二、Kotlin 中的节流实现
1. 使用 sample 操作符(推荐)
Kotlin 1.7.0+ 提供了 sample 操作符,这是最接近传统节流概念的实现:
kotlin
fun main() = runBlocking {
val flow = flow {
repeat(10) {
emit(it)
delay(100) // 每100ms发射一个值
}
}
flow
.onEach { println("原始值: $it at ${System.currentTimeMillis()}") }
.sample(300) // 每300ms采样一次最新值
.collect { println("采样值: $it at ${System.currentTimeMillis()}") }
// 输出:
// 原始值: 0 at t
// 原始值: 1 at t+100
// 原始值: 2 at t+200
// 采样值: 2 at t+300 (窗口内的最后一个值)
// 原始值: 3 at t+300
// 原始值: 4 at t+400
// 原始值: 5 at t+500
// 采样值: 5 at t+600
}
2. 自定义 throttleFirst
实现时间窗口内的第一个事件:
kotlin
fun <T> Flow<T>.throttleFirst(windowMillis: Long): Flow<T> = flow {
var lastEmissionTime = 0L
collect { value ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmissionTime >= windowMillis) {
emit(value)
lastEmissionTime = currentTime
}
}
}
// 使用示例
fun main() = runBlocking {
val fastFlow = flow {
repeat(20) {
emit("Event-$it")
delay(50) // 每50ms一次
}
}
fastFlow
.throttleFirst(200) // 200ms内只取第一个
.collect { println("Throttle First: $it at ${System.currentTimeMillis() % 1000}") }
// 输出:
// Event-0 at t
// Event-4 at t+200 (因为每50ms一次,200ms后是第4个)
// Event-8 at t+400
// ...
}
3. 自定义 throttleLast
实现时间窗口内的最后一个事件:
kotlin
fun <T> Flow<T>.throttleLast(windowMillis: Long): Flow<T> = channelFlow {
coroutineScope {
val buffer = Channel<T>(Channel.CONFLATED)
var lastValue: T? = null
// 生产者
launch {
collect { value ->
buffer.send(value)
}
buffer.close()
}
// 消费者/节流器
launch {
while (!buffer.isClosedForReceive) {
delay(windowMillis)
try {
lastValue = buffer.receive()
// 接收直到最后一个可用值
while (true) {
lastValue = buffer.tryReceive().getOrNull() ?: break
}
lastValue?.let { send(it) }
} catch (e: ClosedReceiveChannelException) {
// 通道已关闭
break
}
}
}
}
}
三、不同节流策略的实现
1. 带缓冲区的节流
kotlin
fun <T> Flow<T>.throttleLatest(
periodMillis: Long,
bufferSize: Int = 0
): Flow<T> = flow {
val buffer = Channel<T>(bufferSize)
var lastEmissionTime = 0L
coroutineScope {
// 收集协程
launch {
collect { value ->
buffer.send(value)
}
buffer.close()
}
// 节流协程
while (!buffer.isClosedForReceive) {
val timeSinceLastEmission = System.currentTimeMillis() - lastEmissionTime
val delayTime = maxOf(0, periodMillis - timeSinceLastEmission)
if (delayTime > 0) {
delay(delayTime)
}
try {
val value = buffer.receive()
emit(value)
lastEmissionTime = System.currentTimeMillis()
// 清空缓冲区,只保留最新的
var latest = value
while (true) {
latest = buffer.tryReceive().getOrNull() ?: break
}
if (latest != value) {
emit(latest)
lastEmissionTime = System.currentTimeMillis()
}
} catch (e: ClosedReceiveChannelException) {
break
}
}
}
}
2. 基于数量的节流
kotlin
fun <T> Flow<T>.throttleByCount(count: Int): Flow<List<T>> = flow {
val buffer = mutableListOf<T>()
collect { value ->
buffer.add(value)
if (buffer.size >= count) {
emit(buffer.toList())
buffer.clear()
}
}
// 处理剩余数据
if (buffer.isNotEmpty()) {
emit(buffer.toList())
}
}
// 使用示例
flowOf(1, 2, 3, 4, 5, 6, 7)
.throttleByCount(3)
.collect { println("每3个一组: $it") }
// 输出: [1, 2, 3], [4, 5, 6], [7]
四、实际应用场景
1. 按钮防连点
kotlin
class ButtonViewModel {
private val _clicks = MutableSharedFlow<Unit>()
val safeClicks = _clicks
.throttleFirst(1000) // 1秒内只能点击一次
.onEach {
performAction()
println("点击处理 at ${System.currentTimeMillis()}")
}
suspend fun onButtonClick() {
_clicks.emit(Unit)
}
private fun performAction() {
// 执行按钮点击操作
}
}
// 使用
fun main() = runBlocking {
val viewModel = ButtonViewModel()
// 模拟快速点击
launch {
repeat(5) {
viewModel.onButtonClick()
delay(300) // 每300ms点击一次
}
}
delay(2000)
}
2. 滚动事件处理
kotlin
class RecyclerViewScrollHandler {
private val _scrollEvents = MutableSharedFlow<ScrollEvent>()
val throttledScrollEvents = _scrollEvents
.throttleLatest(16) // ~60fps,16ms一次
.onEach { event ->
updateUI(event.position)
}
fun onScroll(position: Int) {
viewModelScope.launch {
_scrollEvents.emit(ScrollEvent(position))
}
}
data class ScrollEvent(val position: Int)
}
3. 传感器数据采样
kotlin
class SensorManager {
private val _sensorData = MutableSharedFlow<SensorData>()
val processedData = _sensorData
.throttleFirst(100) // 100ms采样一次
.filter { data -> data.isValid() }
.distinctUntilChanged { old, new ->
// 变化显著才处理
abs(old.value - new.value) > 0.1
}
.onEach { data ->
updateDisplay(data)
if (shouldSave(data)) {
saveToDatabase(data)
}
}
.shareIn(
scope = CoroutineScope(Dispatchers.IO),
started = SharingStarted.Lazily,
replay = 1
)
fun onSensorUpdate(data: SensorData) {
viewModelScope.launch {
_sensorData.emit(data)
}
}
}
4. 实时位置更新
kotlin
class LocationTracker {
private val _locationUpdates = MutableSharedFlow<Location>()
val optimizedUpdates = _locationUpdates
.throttleFirst(5000) // 5秒更新一次
.filter { location ->
location.accuracy < 50.0 // 精度过滤
}
.distinctUntilChanged { old, new ->
// 移动超过10米才更新
old.distanceTo(new) < 10.0
}
.onEach { location ->
updateMapMarker(location)
logLocation(location)
}
}
5. 实时搜索建议
kotlin
class SearchSuggestionViewModel {
private val _searchQuery = MutableStateFlow("")
val suggestions = _searchQuery
.filter { it.length >= 2 }
.throttleLatest(300) // 300ms内的最新查询
.distinctUntilChanged()
.flatMapLatest { query ->
getSuggestions(query)
.catch { emit(emptyList()) }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
private fun getSuggestions(query: String): Flow<List<String>> = flow {
// 模拟API调用
delay(200)
emit(listOf("$query suggestion1", "$query suggestion2"))
}
}
五、与防抖结合使用
1. 防抖+节流组合
kotlin
fun <T> Flow<T>.debounceAndThrottle(
debounceTime: Long,
throttleTime: Long
): Flow<T> = flow {
val debounced = this@debounceAndThrottle
.debounce(debounceTime)
val throttled = this@debounceAndThrottle
.throttleFirst(throttleTime)
merge(debounced, throttled)
.distinctUntilChanged()
.collect { emit(it) }
}
// 使用场景:既要快速响应,又要防止过度触发
inputFlow
.debounceAndThrottle(
debounceTime = 1000, // 停止后1秒执行
throttleTime = 200 // 但至少200ms响应一次
)
.collect { /* ... */ }
2. 智能节流策略
kotlin
class AdaptiveThrottler<T>(
private val initialPeriod: Long = 1000L,
private val minPeriod: Long = 100L,
private val maxPeriod: Long = 5000L
) {
private var currentPeriod = initialPeriod
private var lastUpdateTime = 0L
fun createFlow(source: Flow<T>): Flow<T> = flow {
var lastValue: T? = null
var pendingValue: T? = null
source.collect { value ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastUpdateTime >= currentPeriod) {
// 可以立即发射
emit(value)
lastUpdateTime = currentTime
adjustPeriod(true) // 增加频率
} else {
// 存储为待处理值
pendingValue = value
}
lastValue = value
}
// 处理最后一个待处理值
pendingValue?.let { emit(it) }
}
private fun adjustPeriod(wasEmitted: Boolean) {
if (wasEmitted) {
// 成功发射,可以尝试增加频率
currentPeriod = maxOf(minPeriod, currentPeriod - 100)
} else {
// 没有发射,减少频率
currentPeriod = minOf(maxPeriod, currentPeriod + 100)
}
}
}
六、性能优化和注意事项
1. 上下文管理
kotlin
// 正确:在后台进行节流计算
sensorFlow
.throttleFirst(100)
.flowOn(Dispatchers.Default) // 节流操作在后台线程
.collectOn(Dispatchers.Main) // 在主线程收集结果
// 避免在主线程进行节流计算
2. 内存管理
kotlin
class ThrottledFlowManager {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 使用 SharedFlow 共享节流结果
val throttledFlow = sourceFlow
.throttleFirst(500)
.shareIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5000),
replay = 1
)
fun cleanup() {
scope.cancel()
}
}
3. 错误处理
kotlin
dataFlow
.throttleFirst(1000)
.catch { e ->
// 节流过程中的错误
println("Throttle error: $e")
emit(defaultValue)
}
.onCompletion {
println("Throttled flow completed")
}
.collect { /* ... */ }
4. 测试节流行为
kotlin
@Test
fun testThrottleFirst() = runTest {
val flow = flow {
emit(1)
delay(50)
emit(2)
delay(150) // 总共200ms
emit(3)
}
val results = mutableListOf<Int>()
val job = launch {
flow.throttleFirst(200).collect { results.add(it) }
}
advanceTimeBy(300)
job.cancel()
assertEquals(listOf(1, 3), results)
}
七、与其他操作符的对比
| 操作符 | 行为 | 适用场景 | 示例 |
|---|---|---|---|
| sample(period) | 定期采样最新值 | 数据监控、采样 | .sample(1000) |
| debounce(timeout) | 等待稳定后执行 | 搜索输入、保存 | .debounce(300) |
| throttleFirst(time) | 时间窗口第一个 | 按钮点击、滚动 | .throttleFirst(1000) |
| throttleLatest(time) | 时间窗口最后一个 | 实时更新、聊天 | 自定义实现 |
| distinctUntilChanged() | 值变化时发射 | 状态更新、过滤重复 | .distinctUntilChanged() |
八、最佳实践总结
-
选择合适的节流策略
throttleFirst:用于按钮点击、防止重复提交throttleLatest/sample:用于实时数据流、UI更新- 自定义节流:特殊需求时使用
-
合理设置时间间隔
kotlin// UI更新:16ms (60fps) .throttleFirst(16) // 按钮防连点:300-1000ms .throttleFirst(1000) // 网络请求:200-500ms .throttleLatest(300) // 数据保存:1000-2000ms .throttleFirst(2000) -
结合其他操作符
kotlinflow .filter { /* 条件过滤 */ } .throttleFirst(timeout) // 节流 .distinctUntilChanged() // 去重 .catch { /* 错误处理 */ } .flowOn(Dispatchers.Default) // 指定上下文 .collect { /* 处理结果 */ } -
监控性能
kotlinflow .throttleFirst(100) .onStart { println("开始节流") } .onEach { println("节流输出: $it") } .onCompletion { println("节流完成") }
节流是优化应用性能、改善用户体验的重要工具。合理使用节流可以:
- 减少不必要的计算
- 防止界面卡顿
- 降低服务器压力
- 提供更流畅的用户交互