一、核心基础:冷流 vs 热流(最关键的底层逻辑)
1. 本质定义(判断标准)
| 维度 | 冷流(Cold Flow) | 热流(Hot Flow) |
|---|---|---|
| 核心特征 | 订阅驱动:有订阅者才启动上游,无订阅者上游停止 | 独立运行:上游不依赖订阅者,有无订阅者都可能运行 |
| 订阅者 - 上游关系 | 一对一:每个订阅者触发独立的上游实例 | 一对多:所有订阅者共享同一个上游实例 |
| 数据接收规则 | 订阅后才能收到后续发射的数据(错过的收不到) | 无论何时订阅,都能收到订阅后的新数据(可配置缓存) |
| 典型代表 | 普通 Flow(flow {...})、flowOf、asFlow 等 | StateFlow、SharedFlow、Channel.asFlow ()(特殊) |
2. 代码验证(一眼看懂区别)
kotlin
// 冷流示例:每个订阅者触发一次上游(打印一次"冷流上游启动")
val coldFlow = flow {
println("冷流上游启动") // 订阅时才执行
emit(1)
emit(2)
}
// 订阅1次 → 打印"冷流上游启动"
coldFlow.collect { println("订阅者1:$it") }
// 订阅2次 → 再次打印"冷流上游启动"(上游重复执行)
coldFlow.collect { println("订阅者2:$it") }
// 热流示例:所有订阅者共享上游(只打印一次"热流上游启动")
val hotFlow = coldFlow.shareIn(
scope = CoroutineScope(Dispatchers.Default),
started = SharingStarted.Eagerly,
replay = 1
)
// 订阅1次 → 打印"热流上游启动"
hotFlow.collect { println("热流订阅者1:$it") }
// 订阅2次 → 不打印"热流上游启动"(共享已有上游)
hotFlow.collect { println("热流订阅者2:$it") }
3. 关键结论
- 冷流的核心问题:多订阅者会重复执行上游逻辑(比如多次发起网络请求、多次监听 WebSocket 状态),这是 "性能浪费" 的根源;
- 热流的核心价值:共享上游,无论多少订阅者,上游只执行一次。
二、热流的具体实现:SharedFlow vs StateFlow
StateFlow 是特殊的 SharedFlow,两者都是热流,核心区别如下:
| 特性 | SharedFlow | StateFlow |
|---|---|---|
| 初始值 | 可选(无默认值) | 必须有初始值(创建时指定) |
| 缓存(replay) | 可自定义(比如 replay=3 缓存 3 个值) | 固定 replay=1(只缓存最新值) |
| 发射规则 | 可以发射相同值(多次 emit (1) 都会触发) | 只发射与当前值不同的新值(emit (1) 后再 emit (1) 不触发) |
| 典型场景 | 事件通知(比如点击事件、网络请求结果) | 状态存储(比如 UI 状态、WebSocket 连接状态、网络请求 ID) |
代码示例
kotlin
// 1. StateFlow(存储网络请求ID状态,有初始值)
val _currentRequestId = MutableStateFlow<String?>(null) // 必须有初始值
val currentRequestId: StateFlow<String?> = _currentRequestId
// 只有值变化时才触发订阅者(重复emit相同值不触发)
_currentRequestId.value = "req_123" // 触发订阅者
_currentRequestId.value = "req_123" // 不触发订阅者
// 2. SharedFlow(发送WebSocket连接事件,可重复发射)
val _webSocketEvents = MutableSharedFlow<WebSocketEvent>(
replay = 0, // 不缓存事件,错过就没了
extraBufferCapacity = 0
)
val webSocketEvents: SharedFlow<WebSocketEvent> = _webSocketEvents
// 重复发射相同事件也会触发订阅者
_webSocketEvents.emit(WebSocketEvent.Connected)
_webSocketEvents.emit(WebSocketEvent.Connected)
三、关键操作符:是否改变 Flow 的冷热属性?
核心结论 :combine/map/filter/merge/zip/debounce 等所有基础操作符,都不会改变原 Flow 的冷热属性,也不会自动共享上游。
1. 重点验证:combine 对冷热属性的影响
kotlin
// 上游1:StateFlow(热流)
val apiInfoFlow: StateFlow<ApiResponse?> = apiService.infoFlow
// 上游2:普通Flow(冷流)
val socketFlow: Flow<WebSocketState> = webSocketManager.socketFlow
// combine后的流:冷流!(只要有一个上游是冷流,combine后就是冷流)
val combinedFlow = combine(apiInfoFlow, socketFlow) { requestData, state ->
// 这个lambda,每个订阅者都会触发独立执行
println("combine逻辑执行")
Pair(requestData, state)
}
// 订阅1次 → 打印"combine逻辑执行"
combinedFlow.collect { ... }
// 订阅2次 → 再次打印"combine逻辑执行"(上游重复执行)
combinedFlow.collect { ... }
2. 各操作符的统一规则
| 操作符 | 是否改变冷热属性 | 是否重复执行上游 | 核心影响 |
|---|---|---|---|
| map/filter | 否 | 是(冷流场景) | 只是转换 / 过滤数据,不共享 |
| combine/merge/zip | 否 | 是(冷流场景) | 包装多个流,冷流特性保留 |
| debounce/distinctUntilChanged | 否 | 是(冷流场景) | 只是限流 / 去重,不共享 |
| shareIn/stateIn | 是(冷→热) | 否(热流场景) | 转为热流,共享上游 |
四、性能优化:避免上游 "多开"
核心诉求是 "不浪费性能",本质就是避免冷流的多订阅者重复执行上游 ,唯一解决方案:shareIn/stateIn 将冷流转热流。
1. 最优写法(通用场景)
kotlin
// 步骤1:定义冷流(combine后的流)
val coldCombinedFlow = combine(
apiService.infoFlow, // 可能是StateFlow(热)
webSocketManager.socketFlow // 可能是冷流
) { requestInfo, _ ->
// 业务逻辑处理
Pair(requestInfo, _)
}.filterNotNull()
// 步骤2:转为热流(核心:避免多订阅者重复执行combine逻辑)
val hotCombinedFlow = coldCombinedFlow.shareIn(
scope = appCoroutineScope, // 建议用生命周期绑定的Scope(如viewModelScope)
started = SharingStarted.WhileSubscribed(5000), // 5秒超时释放
replay = 1 // 缓存最新值,新订阅者立即拿到
)
// 多次订阅 → 上游combine逻辑只执行一次
hotCombinedFlow.collect { ... } // 第一次订阅启动上游
hotCombinedFlow.collect { ... } // 第二次订阅共享上游,不重复执行
2. WhileSubscribed(5000) 的深层价值
- 避免 "页面旋转 / 短时间切后台" 导致上游重启(5 秒内重新订阅,上游不停止);
- 避免 "长时间无订阅" 导致资源浪费(5 秒后停止上游,释放网络请求 / 网络监听)。
3. 实战避坑点
- 误区:"上游是 StateFlow(热流),combine 后就是热流" → 错!combine 后的流依然是冷流,多订阅者会重复执行 combine 内的逻辑;
- 误区:"加了 filterNotNull 就是热流" → 错!filter 只是过滤,不改变冷热属性;
- 关键 :只要流需要被多个地方订阅(比如 Fragment+ViewModel),就必须用 shareIn/stateIn 转为热流。
总结
冷热流核心本质 :冷流是 "数据产生逻辑的封装",本身不主动产生数据,仅在调用collect订阅时执行内部逻辑、创建独立的 "数据生产者",多订阅者会触发多个独立生产者(如多次发起独立网络请求),适合需要 "多次独立执行" 的场景;热流的核心是 "全局唯一的数据流",上游不依赖订阅者独立运行,所有订阅者共享同一个生产者,能从根源避免重复执行上游逻辑造成的性能浪费。
操作符核心规则 :map、combine、filter 等基础操作符不会改变 Flow 的冷热属性,冷流经其处理后仍为冷流;只有shareIn/stateIn能将冷流转为热流,是实现上游共享的唯一方式。
性能优化核心方案 :多订阅场景(如 Fragment+ViewModel 共用数据流)下,需通过shareIn/stateIn将冷流(尤其是 combine 后的冷流)转为热流;配合WhileSubscribed(5000)使用可平衡响应速度与资源释放,既避免短时间页面切换导致的上游重启,又能在长时间无订阅时释放网络、WebSocket 监听等资源。
热流选型原则 :基于数据类型选择适配的热流实现 ------ 状态型数据(如网络请求 ID、WebSocket 连接状态)优先用StateFlow(有初始值、固定 replay=1、自动去重重复值,适配 "存储单一最新状态" 需求);事件型数据(如网络请求结果、WebSocket 连接事件)优先用SharedFlow(无强制初始值、可自定义缓存、支持重复发射相同事件);已有冷流需转为全局唯一数据流时,用shareIn配合生命周期 Scope、WhileSubscribed(5000)和合理的 replay 值改造。