Flow 责任链模式图解
背景:FCM 推送消息过滤问题
实际场景
在我们的应用中遇到一个典型问题:
问题描述:
- FCM(Firebase Cloud Messaging)推送消息
- 一分钟内可能发送多个相同的消息
- 每次收到消息就会触发网络请求
- 导致重复请求,浪费带宽和服务器资源
解决方案:
- 需要过滤重复消息,在一分钟内多个消息只有第一个响应
- 实现两个 Flow 操作符来解决不同场景的问题
kotlin
/**
* 冷流节流操作符:在时间窗口内只执行第一次,避免频繁执行
* @param durationMillis 节流时间窗口(毫秒),在此时间内只会执行一次
* @return Flow<T> 应用节流后的Flow
*/
fun <T> Flow<T>.throttleFirst(
durationMillis: Long
): Flow<T> {
var lastCurrentTime = 0L
return flow {
val currentTime = System.currentTimeMillis()
if (currentTime - lastCurrentTime > durationMillis) {
lastCurrentTime = currentTime
// 收集并缓存所有发射的值
this@throttleFirst.collect { value ->
emit(value)
}
}
}
}
```kotlin
/*
热流
@param durationMillis 节流时间窗口(毫秒)
* @param predicate 判断条件,返回 true 表示该值需要节流处理,false 表示直接通过
* @return Flow<T> 应用条件节流后的 Flow
*/
fun <T> Flow<T>.throttle(
durationMillis: Long,
predicate: ((T) -> Boolean)? = null
): Flow<T> {
var lastEmitTime = 0L
return filter { value ->
if (predicate != null && !predicate(value)) {
// 不满足条件的值直接通过,不受节流限制
true
} else {
// 满足条件的值应用节流逻辑
val currentTime = System.currentTimeMillis()
(currentTime - lastEmitTime > durationMillis).also { shouldEmit ->
if (shouldEmit) {
lastEmitTime = currentTime
}
}
}
}
}
一、Flow 的基本结构
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Source │─────▶│ Operator 1 │─────▶│ Operator 2 │─────▶│ Collector │
│ Flow │ │ (map) │ │ (filter) │ │ (collect) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
上游 中间操作符 中间操作符 下游
二、flow { } 构建器的原理
函数签名
kotlin
fun <T> flow(block: suspend FlowCollector<T>.() -> Unit): Flow<T>
block 参数的作用
block是FlowCollector<T>的扩展函数- 相当于重写了
collect方法 - 可以决定是否调用上游的
collect
责任链的关键
是否调用上游 collect?
│
┌─────────┴─────────┐
▼ ▼
调用了 不调用
│ │
┌───────┴───────┐ │
▼ ▼ ▼
上游 Flow 执行 上游产生数据 上游不执行
耗费资源 传递给下游 节省资源
(责任链断开)
三、冷流 vs 热流对比
冷流实现(throttleFirst)
时间线: 0ms 500ms 1200ms
│ │ │
调用: collect1 collect2 collect3
│ │ │
判断: ✓ 执行 ✗ 跳过 ✓ 执行
│ │ │
上游: 执行 不执行 执行
│ │
结果: 发射数据 发射数据
代码流程:
┌─────────────────────────────────┐
│ flow { │
│ if (时间满足) { │
│ this@throttleFirst.collect {│◀─ 只有条件满足时才调用
│ emit(it) │
│ } │
│ } │
│ // 条件不满足,不调用 collect │◀─ 上游不执行
│ } │
└─────────────────────────────────┘
特点:
✓ 可以完全跳过上游执行
✓ 节省网络、数据库、计算等资源
✓ 适合按需执行的耗时操作
热流实现(throttle)
时间线: 0ms 100ms 200ms 300ms 400ms
│ │ │ │ │
上游: ① ② ③ ④ ⑤
│ │ │ │ │
判断: ✓发射 ✗过滤 ✗过滤 ✓发射 ✗过滤
│ │ │
下游: ① ④
代码流程:
┌─────────────────────────────────┐
│ flow { │
│ this@throttle.collect { value│◀─ 始终调用上游 collect
│ if (时间满足) { │
│ emit(value) │◀─ 选择性发射
│ } │
│ // 上游持续执行,只是不发射 │
│ } │
│ } │
└─────────────────────────────────┘
特点:
✓ 上游持续执行
✓ 数据持续产生
✓ 只是选择性发射给下游
✓ 适合持续的数据流(事件、传感器等)
四、责任链的断开与延续
场景 1:责任链正常执行
┌────────┐ collect ┌────────┐ collect ┌────────┐ collect ┌────────┐
│ Source │────────────▶│ Map │────────────▶│ Filter │────────────▶│Collect │
└────────┘ └────────┘ └────────┘ └────────┘
│ │ │ │
▼ ▼ ▼ ▼
执行 执行 执行 接收
发射数据 转换数据 过滤数据 处理数据
代码:
source.collect { value1 -> // Collector 调用 source.collect
emit(transform(value1)) // Source 发射 → Map 的 FlowCollector
}
map.collect { value2 -> // Map 调用 filter.collect
if (predicate(value2)) { // Filter 的 FlowCollector
emit(value2)
}
}
filter.collect { value3 -> // Filter 调用 最终 collect
handleData(value3) // 最终的处理逻辑
}
场景 2:责任链被拦截
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Source │ ✗ │ Map │ ✗ │ Filter │ ✗ │Collect │
└────────┘ └────────┘ └────────┘ └────────┘
│ │ │ │
▼ ▼ ▼ ▼
不执行 不执行 不执行 无数据
┌────────────┐
│ Intercept │◀─ 拦截点:不调用上游 collect
└────────────┘
│
▼
条件不满足
不调用 collect
责任链断开
代码:
flow {
if (!condition) {
// 不调用 this@intercept.collect
// 上游 Source 不会执行
return@flow
}
this@intercept.collect { value ->
emit(value)
}
}
五、FlowCollector 的角色
FlowCollector 是责任链的节点
每个操作符创建一个新的 FlowCollector:
┌──────────────────────────────────────────────────────────────┐
│ Flow 责任链 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ FlowCollector│ │ FlowCollector│ │
│ │ #1 │ │ #2 │ │
│ │ │ │ │ │
│ │ collect { │───────▶│ collect { │ │
│ │ emit(...)─┼───┐ │ emit(...)─┼───┐ │
│ │ } │ │ │ } │ │ │
│ └──────────────┘ │ └──────────────┘ │ │
│ │ │ │
│ Source Flow 接收 Map Flow 接收 │
│ │ │ │
│ └────────────────────────┘ │
│ 数据流动方向 │
└──────────────────────────────────────────────────────────────┘
FlowCollector 的两个关键方法
kotlin
interface FlowCollector<T> {
// 发射数据给下游
suspend fun emit(value: T)
}
interface Flow<T> {
// 收集上游的数据
suspend fun collect(collector: FlowCollector<T>)
}
数据流动过程
1. 最终的 collect 被调用:
flow.collect { value ->
println(value)
}
2. 触发最后一个操作符的 collect:
filter.collect(FlowCollector { value ->
println(value)
})
3. filter 内部调用上游的 collect:
this@filter.collect { value ->
if (predicate(value)) {
emit(value) // 发送给下游的 FlowCollector
}
}
4. 依次向上,直到 Source Flow:
sourceFlow.collect { value ->
emit(value) // 开始向下发送数据
}
5. 数据从 Source 流向下游:
Source.emit → Collector#1.emit → Collector#2.emit → ... → 最终 collect
六、性能优化总结
冷流优化
场景:按钮快速点击 5 次,触发网络请求
不使用 throttleFirst:
┌─────┬─────┬─────┬─────┬─────┐
│Click│Click│Click│Click│Click│
└──┬──┴──┬──┴──┬──┴──┬──┴──┬──┘
▼ ▼ ▼ ▼ ▼
请求1 请求2 请求3 请求4 请求5
(浪费了 4 个请求)
使用 throttleFirst(1000ms):
┌─────┬─────┬─────┬─────┬─────┐
│Click│Click│Click│Click│Click│
└──┬──┴─✗───┴─✗───┴─✗───┴──┬──┘
▼ ▼
请求1 请求5
(只发起 2 个请求,节省 3 个)
热流优化
场景:位置每 100ms 更新一次,UI 只需 500ms 更新
不使用 throttle:
时间: 0 100 200 300 400 500 600
位置: ①───②───③───④───⑤───⑥───⑦
UI: 更新 更新 更新 更新 更新 更新 更新
(频繁更新,性能浪费)
使用 throttle(500ms):
时间: 0 100 200 300 400 500 600
位置: ①───②───③───④───⑤───⑥───⑦
│ ✗ ✗ ✗ ✗ │ ✗
UI: 更新 更新
(减少UI刷新,提升性能)
七、核心要点
1. 责任链的核心
┌────────────────────────────────────────┐
│ 调用上游 collect? │
├────────────────────────────────────────┤
│ │
│ YES → 责任链延续 │
│ 上游执行,数据流动 │
│ 可以处理、转换、过滤数据 │
│ │
│ NO → 责任链断开 │
│ 上游不执行,节省资源 │
│ 适合条件拦截、节流 │
│ │
└────────────────────────────────────────┘
2. 冷流 vs 热流
| 特性 | 冷流 (throttleFirst) | 热流 (throttle) |
|---|---|---|
| 上游执行 | 条件控制 | 始终执行 |
| 判断时机 | 调用 collect 前 | emit 时 |
| 资源消耗 | 可以跳过 | 持续产生 |
| 适用场景 | 按需计算 | 持续数据流 |
| 典型例子 | 网络请求 | UI 事件 |
3. FlowCollector 的作用
┌─────────────────────────────────────┐
│ FlowCollector 是责任链的节点 │
├─────────────────────────────────────┤
│ │
│ 1. collect() - 从上游收集数据 │
│ 2. emit() - 向下游发射数据 │
│ 3. 决定是否调用上游的 collect │
│ 4. 决定是否调用下游的 emit │
│ │
└─────────────────────────────────────┘
4. 使用建议
┌──────────────┬────────────────────────┐
│ 使用冷流 │ 使用热流 │
├──────────────┼────────────────────────┤
│ ✓ 网络请求 │ ✓ UI 事件流 │
│ ✓ 数据库操作 │ ✓ 传感器数据 │
│ ✓ 文件 I/O │ ✓ WebSocket 消息 │
│ ✓ 复杂计算 │ ✓ 实时数据更新 │
│ ✓ 按需执行 │ ✓ 持续数据源 │
└──────────────┴────────────────────────┘
总结
Flow 的责任链模式核心在于:
- flow { } 构建器的 block 参数决定是否调用上游 collect
- 冷流:条件不满足时不调用 collect,上游不执行,节省资源
- 热流:始终调用 collect,只在 emit 时判断,适合持续数据流
- 责任链断开:不调用上游 collect,整个上游链都不执行
- 责任链延续:调用上游 collect,数据在各个 FlowCollector 间流动