Kotlin Flow/Sequence/Channel 实战原理与场景详解
1. 基本概念对比
- Flow:数据流(连续),同协程下的数据序列持续发送与处理。
- SharedFlow:事件流(独立),支持事件序列向多个订阅者(跨协程)一对多通知。
- StateFlow:状态流(独立),是特殊SharedFlow,能订阅当前状态,本质上也是事件。
2. Sequence的机制和与List的区别
Flow,可以称之为协程版本的sequence,类似于队列的机制,队头插入数据,队尾取出数据,其实只要有这样的机制的数据结构我们都可以称之为队列,例如LinkedList。Sequence就是按照队列的方式来提供数据,并且按照提供的顺序来获取数据的Api,但和队列的区别在于,它不是数据结构,而是一种机制。因为它让我提供的是数据的规则,而不是数据的本身,我们用Sequence来提供的数据,实际上是动态生产的,用完一条才生产下一条,而不是像队列那样,提前把数据都准备好了,然后一个一个取用。当然队列也可以实现容量是1,就是生产一条取一条。不生产就没有。Sequence是从机制上就限制了,生产一条使用一条,使用完成之后再去生产一条新的数据的机制。
val nums = sequence {
while (true) {
yield(1)
}
}
协程里边有一个yield函数,它是手动的让协程暂停一下,和线程的Thread.yield()是一样的定位。通过强行让自己暂停的方式让出当前暂用的线程。来手动调节线程占用的,本质上就是在当前时间片还没有到时间的时候,就主动放弃时间片。属于很底层的函数。Sequence里边也有yield的函数,它是用来生产数据的,和协程的yield完全无关。
小结:
- Sequence没有内部数据,只是“生成规则”,只有遍历时才会产生数据。
- List是全部数据预先存好,遍历只是读取。
- Sequence更适合“惰性生成”/数据量巨大/无限流的场景。
3. Sequence和List的懒加载举例
val list = buildList {
while (true) { // list 永远无法执行完,直到内存溢出。
add(getData())
}
}
for (num in list) {
println("List item: $num")
}
val nums = sequence {// sequence的机制保证了它的数据是使用者需要数据的时候才会生产。
while (true) {
yield(1)
}
}
for (num in nums) {
println("List item: $num")
}
总结补充:
- 用list无限add会OOM;sequence不会,永远只生产用到的那一条数据。
- Sequence没有内部数据,是生产规则。
- Sequence能更快开始处理数据,但如果你全部用完,和list整体性能差不多。
4. Sequence只能用自己的挂起函数,不能用suspend
在sequence里边,是不能调用不属于Sequence类的挂起函数,调用其余的挂起函数都会报错。为什么会有这个限制呢?这是因为kotlin团队希望Sequence使用协程来实现业务逻辑,但这套协程的逻辑是完全独立的、与世隔绝的,避免其他环境的协程代码进入执行时,影响Sequence的初始逻辑。从本质上讲,这种只能用自己挂起函数的限制,是提供了一种场景化的特权。
举例:
val nums = sequence {
// delay() 调用就会报错。
while (true) {
yield(1)
yieldAll()
}
}
结论:
Sequence更适合纯同步流式数据。如果要异步挂起、比如网络请求,就要用Flow。
5. Flow的本质和冷/热流原理
Flow的工作模式:设定好一套逻辑,在每个collect地方都重复执行一遍这个逻辑,在这套逻辑里边安插的一些发送数据的节点,而collect执行这一套逻辑的时候,对于每一条数据都会执行自己设定好的数据处理逻辑,这就是Flow的本质。由Flow对象来提供生产数据流的生产逻辑,然后在收集流程里执行这套生产逻辑,并处理每条数据,所以说Flow就是一个数据流工具。
冷流/热流简述:
- Channel是热流,数据可以在没有collect时就被生产。
- Flow是冷流,只有collect时才生产。
例子:
val numsFlow = flow {
emit(1)
delay(100)
emit(2)
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
numsFlow.collect {
println("A: $it")
}
}
scope.launch {
delay(50)
numsFlow.collect {
println("B: $it")
}
}
6. Flow和Sequence收集方式区别
val nums = sequence {
while (true) { yield(1) }
}
for (num in nums) {
println("List item: $num")
}
// Flow中的Collect方法或报错,因为collect方式是suspend修饰的,必须在协程中执行。
val numsFlow = flow {
while (true) { emit(getData()) }
}.map { "number $it" }
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
numsFlow.collect { println(it) }
}
- Sequence可以普通for-in直接用。
- Flow的collect是挂起的,必须在协程作用域中调用。
7. Flow的适用场景
如果想把数据的提供和数据的处理两个流程拆分开来,就需要使用Flow了。这种持续提供数据的形式,就是数据流,而需要把数据流的生产和消费功能拆分开的业务场景就是Flow的用途所在。
典型代码:
val weatherFlow = flow {
while (true) {
emit(getWeather())
delay(60000)
}
}
suspend fun showWeather(flow: Flow<String>) {
flow.collect {
println("Weather: $it")
}
}
- 持续数据/异步/网络/耗时,适合用Flow。
- 普通同步、只需要一次性数据处理,Sequence/List更简单。
8. Flow的创建方式全汇总
flow { emit(1) } // 1. 最基础
flowOf(1,2,3) // 2. 直接用
listOf(1,2,3).asFlow() // 3. List转Flow
sequenceOf(1,2,3).asFlow() // 4. Sequence转Flow
// 5. 用Channel创建的Flow(注意冷热流区别)
val channel = Channel<Int>()
val flow5 = channel.consumeAsFlow()
val flow6 = channel.receiveAsFlow()
- channel.consumeAsFlow只能消费一次;receiveAsFlow每次collect都新建一个receive。
9. channelFlow 和 callbackFlow 的意义
创建Flow还有两种方式,channelFlow和callbackFlow:
这两种方式与上边的 consumeAsFlow 和receiveAsFlow的区别是,上边两种方式共用了一个Channel,所有数据的消费都需要通过collect进行,会瓜分channel生产的数据。而后边这两种方式,它创建的Flow是直到Collect的时候才会创建Channel,开始生产。也就是,对此调用就会创建多个Channel,这些Channel的生产流程是互相隔离、各自独立的。如果还用冷热概念来说的话,consumeAsFlow 和receiveAsFlow是热的Flow,而 channelFlow 与callbackFlow 是冷的Flow。
channelFlow的作用
- 允许在block里启动子协程、可以跨协程emit数据(Flow不能这样)。
callbackFlow专为回调API适配
- 必须调用awaitClose,否则会报错。
val flow7 = channelFlow {
launch {
delay(2000)
send(2)
}
delay(1000)
send(1)
}
val flow9 = callbackFlow {
gitHub.contributorsCall("square", "retrofit")
.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(call: Call<List<Contributor>>, response: Response<List<Contributor>>) {
trySend(response.body()!!)
close()
}
override fun onFailure(call: Call<List<Contributor>>, error: Throwable) {
cancel(CancellationException(error))
}
})
awaitClose()
}
10. 为什么Flow不能跨协程emit?
这是因为我们的collect在处理数据的时候,是在发送数据的各个协程里边处理的,这也就导致了,各个协程可能所在的Dispatcher是不一样的,最终会导致软件的运行所在线程可能与开发者的预期不一致。处理这个问题有两种方式,一种是上层业务方自己使用的时候注意切换写成,另一种就是底层,从api的设计就限制了flow,禁止在别的协程调用flow来发送数据,也就是emit必须在flow自己的代码块中发送,强制保证数据一定是从collect所在的协程发送的。
核心总结:
- Flow限制emit只能在block当前协程,是为了保证数据顺序和一致性,防止多线程乱序难以排查。
- 如果有多线程/跨协程emit的需求,直接用channelFlow!
11. SharedFlow和StateFlow的本质
SharedFlow 是冷流和热流的中间态,适合事件总线、多个订阅者共享事件流。
StateFlow 是带有“最新状态”的 SharedFlow,订阅者随时能拿到当前值。用在状态驱动(如UI),而不是简单事件。
12. 总结和使用建议
- 有异步、挂起、数据量大、事件流用Flow系列。
- Sequence/List适用于本地、小量、同步数据流。
- 多线程/回调/异步事件流用channelFlow或callbackFlow。
- 只要能用更简单的方式,不要滥用Flow。
学后检测
1. 单选题:下列关于 Sequence 和 List 的区别描述正确的是?
A. Sequence 在声明时会立即生成所有数据
B. List 在遍历时才生产数据
C. Sequence 只有在被消费时才生产数据
D. List 只能存储基本类型数据
答案:C
解析: Sequence是惰性计算,只有被消费(遍历)到时才生产数据;List在构造时所有数据就准备好了。
2. 单选题:下列关于 Kotlin Flow 特性的说法,错误的是?
A. Flow 支持使用挂起函数生产数据
B. Flow 的 collect 必须在协程作用域内调用
C. Flow 一定是热流
D. Flow 可以用来实现持续的异步数据流
答案:C
解析: Flow 默认是冷流,只有 collect 时才会开始生产数据。
3. 判断题:Flow 的 collect 是一个挂起函数,可以直接在主线程上调用。
答案:错
解析: collect 是挂起函数,必须在协程环境下调用,不能在普通主线程上直接用。
4. 单选题:下列哪种方式可以实现跨协程 emit 数据到 Flow?
A. 普通 flow{}
B. channelFlow{}
C. flowOf()
D. listOf().asFlow()
答案:B
解析: 只有 channelFlow{} 支持在内部 block 启动子协程跨协程 emit;普通 flow{} 不能。
5. 多选题:关于 channel.consumeAsFlow() 和 channelFlow{},下列描述正确的是?
A. consumeAsFlow() 返回的 Flow 只能被 collect 一次
B. channelFlow{} 每次 collect 都会新建 Channel
C. consumeAsFlow() 是热流,channelFlow{} 是冷流
D. channelFlow{} 适合回调API转数据流
答案:A B C D(全选)
解析: consumeAsFlow只能collect一次,多次报错;channelFlow每次collect新建channel,是冷流;consumeAsFlow是热流;channelFlow/callbackFlow用于回调API和多协程生产。
6. 简答题:为什么 Kotlin 的 Sequence 不能在 block 内使用 suspend 函数?如果你想在生产数据时用 delay() 应该用什么?
答案参考:
Sequence 的协程上下文是“独立隔离”的,为了防止其它挂起函数改变流程或线程导致混乱,因此被 @RestrictsSuspension 注解限制,只能用 yield 系列。
如果要在生产数据时用 delay() 等挂起函数,应该用 Flow 或 flow{} 构建数据流。
7. 简答题:举例说明 StateFlow 适合解决什么类型的业务场景?请给出使用代码片段。
答案参考:
StateFlow 适合用来做 UI 层的状态同步,比如页面主题切换、登录状态、网络状态等,只要 StateFlow 的值被更新,所有订阅者都会收到最新值。
代码例子:
val stateFlow = MutableStateFlow("未登录")
fun login() {
stateFlow.value = "已登录"
}
scope.launch {
stateFlow.collect { status ->
println("当前登录状态:$status")
}
}
8. 简答题:你用 Flow 实现了一个消息流,结果发现有时候消息被丢了没收集到,可能是什么原因?怎么改进?
答案参考:
原因1:Flow是冷流,每次collect才生产,collect时机不对会丢消息;
原因2:如果用 Channel 转 Flow,多个 collect 会瓜分 channel 数据,导致有些collect拿不到数据。
改进:如果要实现消息广播给多个订阅者,应该用 SharedFlow;如果只是点对点消费,注意不要多个协程同时 collect 同一个 channelFlow/consumeAsFlow。
9. 编程题:用 callbackFlow 封装一个 Android 原生的网络回调API,将回调转换为 Flow,要求所有 collect 都能独立收到数据。
答案参考:
fun retrofitAsFlow(): Flow<String> = callbackFlow {
val call = api.getData()
call.enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
trySend(response.body() ?: "")
close()
}
override fun onFailure(call: Call<String>, t: Throwable) {
cancel("请求失败", t)
}
})
awaitClose { call.cancel() }
}
解析: callbackFlow 每次 collect 都新建一次,互不影响,适合回调转Flow场景。