Flow热流冷流知识梳理

一、核心基础:冷流 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. 实战避坑点
  1. 误区:"上游是 StateFlow(热流),combine 后就是热流" → 错!combine 后的流依然是冷流,多订阅者会重复执行 combine 内的逻辑;
  2. 误区:"加了 filterNotNull 就是热流" → 错!filter 只是过滤,不改变冷热属性;
  3. 关键 :只要流需要被多个地方订阅(比如 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 值改造。

相关推荐
阿巴斯甜8 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker9 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952710 小时前
Andorid Google 登录接入文档
android
黄林晴11 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android