Flow基础:冷流、热流与构建器
在讲泄漏之前,先得把Flow这个基础概念打扎实。我见过不少同学上来就用Flow,结果连冷流热流都分不清,写出来的代码要么莫名其妙不触发,要么一不留神就泄露。说白了,你如果不懂它的“脾气”,就控制不住它。
Flow是什么?它就是一个异步数据流。你可以把它想象成一根水管,数据像水一样从源头流到下游的观察者手里。但这根水管有“冷”和“热”两种模式,搞混了就会出问题。
冷流 vs 热流
我个人习惯这么理解:冷流是“懒”的,热流是“勤快”的。
- 冷流(Cold Flow):每次有新的收集者(collect)时,都会重新执行上游的数据生产逻辑。也就是说,每个订阅者看到的是一份独立的数据序列。
- 热流(Hot Flow):数据生产是独立的,不管你收不收集,数据都在产生。后来的收集者只能看到它开始收集之后的最新数据(或者部分历史数据)。
一句话记牢:冷流是“有人看才演”,热流是“不管你爱不爱看,我都在演”。
我在项目中遇到过好几次泄漏,就是因为把冷流当热流用了,或者反过来。比如你用flow{...}构建一个冷流,结果在Activity里启动了多次collect,数据重复触发,界面刷乱了。
热流呢?最常用的是SharedFlow和StateFlow。SharedFlow适合事件分发(比如按钮点击后的弹窗通知),StateFlow适合状态持有(比如UI状态)。StateFlow其实就是SharedFlow的一个特化版本,它总是保存最近一个值,而且初始化时必须给一个默认值。
Flow构建器
Kotlin协程提供了几种构建Flow的方式,我按使用频率排个序:
| 构建器 | 说明 | 典型场景 |
|---|---|---|
flow { ... } | 最基础的冷流构建器,使用emit发射数据 | 网络请求、数据库查询的序列 |
flowOf(...) | 将固定值集合转成Flow | 测试数据、静态列表 |
asFlow() | 将集合、Sequence、其他类型转成Flow | 从List或Range派发数据 |
callbackFlow { ... } | 将回调式异步API转为Flow | 传感器、位置监听、旧的callback接口 |
channelFlow { ... } | 类似callbackFlow,但使用Channel发送,支持背压 | 需要并发或挂起操作的回调转换 |
嗯,这里要注意:callbackFlow和channelFlow是热流吗?不是,它们本质上还是冷流——每次收集才会启动内部逻辑。但它们内部用了Channel,所以如果你不小心,容易造成资源泄漏。我曾经在callbackFlow里没有在awaitClose中清理监听器,导致Activity销毁后回调依然触发,直接崩了。
常用操作符
操作符就是Flow上的“管道配件”,用来变换、过滤、合并数据流。我用几个经典场景来串讲:
// 1. map — 转换数据
flowOf(1, 2, 3)
.map { it * 2 } // 2, 4, 6
.collect { println(it) }
// 2. filter — 过滤
flowOf(1, 2, 3, 4, 5)
.filter { it % 2 == 0 } // 2, 4
.collect { ... }
// 3. debounce — 防抖,用来处理快速变化的值(比如搜索框输入)
flow {
for (i in 1..10) {
emit(i)
delay(100)
}
}
.debounce(200) // 最后只输出10
.collect { ... }
// 4. zip — 合并两个流,一对一配对
val flowA = flowOf("a", "b", "c")
val flowB = flowOf(1, 2, 3)
flowA.zip(flowB) { a, b -> "$a$b" } // a1, b2, c3
.collect { ... }
// 5. flatMapConcat / flatMapMerge / flatMapLatest — 展平
// 比如一个搜索事件,每次触发一个新的网络请求,用flatMapLatest保证只有最新的请求生效
searchQueryFlow
.flatMapLatest { query -> api.search(query) }
.collect { result -> updateUI(result) }
关于异常处理:冷流中的异常可以用catch操作符捕获,但它只能捕获它之前的异常,不能捕获下游。如果你需要“重试”,用retry或retryWhen。我曾经犯过一个错:直接在collect里try-catch,结果流因为异常终止了,后续数据全丢了。正确姿势是在上游用catch处理并emit一个兜底值。
操作符分为中间操作符和末端操作符。中间操作符返回的是新的Flow(懒执行),末端操作符触发执行(如collect、toList、single、count)。你想想看,如果你只调了.map().filter()但没有collect,数据流永远不会启动——这很符合冷流的“懒”本质。
再敲一次黑板:冷流每次collect都会重新执行上游代码。如果一个flow块里包含网络请求,你collect两次就会发两次请求。如果你只想共享结果,请转成热流(shareIn / stateIn),或者直接用StateFlow。
这张图把Flow分为三大块:冷流构建器、热流实现、操作符。你在写代码时,先想清楚数据流的性质——它是冷还是热?然后选择合适的构建器,最后用操作符搭出你需要的逻辑管道。
我记得有一个经验:如果某个数据流在多个订阅者之间需要共享计算结果(比如同一个网络请求的结果),就转成热流;如果是每个订阅者独立需要完整数据(比如数据库的拉取列表),用冷流。搞反了轻则浪费资源,重则泄漏崩溃。