本文以
FusibleFlow为核心,通过源码片段解析 Flow 的"融合"(fuse)优化:flowOn如何触发融合、fuse如何把多层上下文合并为一次线程切换,以及无法融合时ChannelFlow兜底方案(collectTo)的工作方式。
1. 一个典型的 Flow 调用链示例
scss
flowOf(1, 2, 3)
.map { it * 2 }
.filter { it > 2 }
.flowOn(Dispatchers.IO)
描述 :这是一个典型的冷 Flow 链式调用,也是触发 FusibleFlow 融合的例子:
flowOf(1, 2, 3)创建发射 1、2、3 的 Flow;map { it * 2 }和filter { it > 2 }这类中间操作符,底层都会把 Flow 包装成TransformFlow等实现了FusibleFlow接口的内部类;- 最后调用
flowOn(Dispatchers.IO)时,走到源码里的this is FusibleFlow -> fuse(context = context)分支------由于前面的 map/filter 包装器都是 FusibleFlow,上下文会一路"融合"进整条链,最终不会创建任何 Channel。
也就是说,这条链的处理是:1,2,3 → 2,4,6 → 4,6,全部在 IO 线程上一次发射完成,collect 收集到的就是 4 和 6。注意 flowOn 只影响上游 操作符(flowOf/map/filter)的执行线程,不影响下游 collect 所在的协程。
对比:如果上游不是 FusibleFlow(例如一个自定义的、未实现该接口的 Flow),
flowOn就只能走else分支,创建ChannelFlowOperatorImpl来做线程切换------这正是后面几节要讲的内容。
2. FusibleFlow:可融合 Flow 的内部接口
kotlin
internal interface FusibleFlow<T> : Flow<T> {
fun fuse(context: CoroutineContext): Flow<T>?
}
描述 :这是 kotlinx.coroutines 内部的接口(internal,不对使用者暴露)。它定义了一个 fuse 方法,表示"实现类知道如何把一个新的上下文融合进自己"。fuse 返回 Flow<T>?:
- 返回融合后的新 Flow(或自身)------融合成功;
- 返回
null------ 无法融合。
融合的意义:当多个 flowOn/操作符连续出现时,不必为每一次上下文切换都创建一个 Channel,而是把上下文合并到一起,只需最外层做一次真正的线程切换,大幅减少开销。
3. flowOn 的源码实现
kotlin
public fun <T> Flow<T>.flowOn(context: CoroutineContext): Flow<T> {
checkFlowContext(context)
return when {
context == EmptyCoroutineContext -> this
this is FusibleFlow -> fuse(context = context) // 尝试融合,避免创建 Channel
else -> ChannelFlowOperatorImpl(this, context = context) // 无法融合,创建 ChannelFlow
}
}
描述 :flowOn 的核心逻辑只有三个分支:
checkFlowContext(context):校验传入上下文的合法性(例如不允许包含Job,否则会破坏 Flow 的结构化并发语义)。context == EmptyCoroutineContext:传入的是空上下文,无需切换线程,直接返回原 Flow。this is FusibleFlow:上游本身是可融合的 Flow(如经过flowOn/map等操作符包装后的对象),调用fuse尝试把新上下文融合进上游,避免再创建一层 Channel。else:无法融合,包装成ChannelFlowOperatorImpl------通过 Channel 把上游发射与下游收集解耦,从而实现线程切换。
4. ChannelFlowOperatorImpl:包装器的简化结构
kotlin
// 简化后的源码结构示意
internal class ChannelFlowOperatorImpl<T>(
private val flow: Flow<T>, // 上游 Flow
private val context: CoroutineContext // 需要切换到的协程上下文(例如 flowOn 指定的 Dispatcher)
) : ChannelFlow<T>() {
// ... 内部实现
}
描述 :当无法融合时,flowOn 会把上游 Flow 包成 ChannelFlowOperatorImpl。它持有两个关键成员:
flow:被包装的上游 Flow;context:flowOn指定的目标上下文 (如Dispatchers.IO)。
它继承自 ChannelFlow<T>,核心思路是:上游在一个新协程里发射元素到 Channel,下游从 Channel 接收------Channel 就是线程切换的桥梁。
5. collectTo:ChannelFlow 的核心收集逻辑
kotlin
override suspend fun collectTo(scope: ProducerScope<T>) {
// 1. 在指定的上下文中启动一个新的协程来收集上游 flow
// scope.coroutineContext 包含了 flowOn 指定的 dispatcher
val newContext = scope.coroutineContext + context
// 2. 启动子协程执行上游收集逻辑
// 注意:这里通常使用 scope.launch 或类似机制,确保生命周期绑定
scope.launch(newContext) {
try {
// 收集上游 flow,并将发射的元素发送到 channel (scope.send)
flow.collect { value ->
scope.send(value)
}
} catch (e: Throwable) {
// 异常处理:如果上游抛出异常,需要关闭 channel 并传播异常
scope.close(e)
}
}
// 3. 等待所有子协程完成,确保资源清理
// awaitClose 是 channelFlow/channelFlow 模式中的关键,用于保持 channel 开放直到逻辑结束
scope.awaitClose()
}
描述 :collectTo 是 ChannelFlow 真正干活的地方,分三步:
- 合并上下文 :
scope.coroutineContext + context把flowOn指定的上下文叠加到生产者协程上下文之上,得到newContext------上游收集将在这个上下文中执行,这就是线程切换的落点。 - 启动子协程收集上游 :在
newContext中launch一个子协程,收集上游flow,并把每个发射的元素通过scope.send(value)写入 Channel;若上游抛异常,则scope.close(e)关闭 Channel 并把异常传播给下游。 awaitClose挂起等待:保持 Channel 开放,直到所有子协程结束或下游取消收集。它保证了生产者的生命周期与 Flow 的收集生命周期绑定,防止资源泄漏。
小结 :
flowOn的本质 = 上游在指定上下文的协程中发射 → 经 Channel 传递 → 下游在收集者上下文中接收 。而"融合"优化是在有多层flowOn/操作符时省掉多余的 Channel。
6. ProducerScope:生产者作用域
上面 collectTo(scope: ProducerScope<T>) 中的 scope 就是 ProducerScope。它是 ChannelFlow 体系里的"生产者端协程作用域",定义大致如下:
kotlin
public interface ProducerScope<T> : CoroutineScope {
val channel: SendChannel<T> // 生产者把元素发送进去的 Channel
suspend fun send(value: T) // 便捷方法:等价于 channel.send(value)
fun close(cause: Throwable? = null) // 关闭 Channel,可选地带上异常原因
}
描述 :ProducerScope 同时具备两种身份,这也正是它能撑起整个 Channel 切换机制的原因:
CoroutineScope(协程作用域) :所以才能在collectTo里直接调用scope.launch(newContext)启动子协程、通过scope.coroutineContext读取上下文。子协程自动挂在这个作用域下,随 Flow 收集的取消而一起取消,实现生命周期绑定。- Channel 的"发送端"(
SendChannel) :scope.send(value)把上游发射的元素写入内部 Channel,下游则从 Channel 的接收端读取。scope.close(e)用于在异常时关闭 Channel 并把异常传播给下游。
三个成员在 collectTo 中的对应关系:
| 成员 | 在 collectTo 中的用途 |
|---|---|
coroutineContext |
与 flowOn 指定的 context 合并,作为上游收集的执行上下文 |
send(value) |
把上游发射的每个元素写入 Channel,交给下游 |
launch { ... } |
继承自 CoroutineScope,在目标上下文中启动收集上游的子协程 |
补充 :
channelFlow { }构建器的 lambda 的接收者也是ProducerScope<T>------channelFlow就是"把一个 ProducerScope 暴露给用户来手动 send/launch"的上层封装;而flowOn无法融合时走的ChannelFlow则是在内部自动完成这些 send/launch 操作。两者共用同一套底层机制。
示例 :通过 channelFlow 直接以 ProducerScope 作为接收者,可以最直观地看到它的两种身份如何配合使用:
kotlin
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
fun mergeFlows(flow1: Flow<Int>, flow2: Flow<String>): Flow<String> = channelFlow {
// 身份 1:CoroutineScope ------ 这里 this 就是 ProducerScope<String>,
// 所以可以直接 launch 子协程,作用域内调用 send(value) 不需要写 scope. 前缀
// 启动一个子协程收集 flow1 并发送
val job1 = launch {
flow1.collect { value ->
send("Flow1: $value") // 使用 ProducerScope 的 send 方法
}
}
// 启动另一个子协程收集 flow2 并发送
val job2 = launch {
flow2.collect { value ->
send("Flow2: $value")
}
}
// awaitClose 确保在所有子协程完成之前,Flow 保持活跃
// 当下游取消收集时,awaitClose 会触发,从而取消 job1 和 job2
awaitClose {
job1.cancel()
job2.cancel()
}
}
// 使用示例
suspend fun main() {
val flowA = flowOf(1, 2, 3)
val flowB = flowOf("A", "B")
mergeFlows(flowA, flowB).collect {
println(it)
}
}
描述 :这个例子把 ProducerScope 的两个身份都串了起来:
- 作为协程作用域 :
channelFlow的 lambda 里this就是ProducerScope<String>,所以launch可以直接调用------job1和job2两个子协程并发收集flow1和flow2。这也是channelFlow能合并多流的根本原因:普通flow { }构建器不允许并发发射,而ProducerScope提供的作用域让多个子协程可以同时往同一个 Channel 发送; - 作为 Channel 发送端 :
send("Flow1: $value")把元素写入内部 Channel,下游collect在自己的协程中接收------两个上游的发射与下游的接收分别运行在不同协程,靠 Channel 传递; - 生命周期 :
awaitClose挂起生产者协程保持 Flow 活跃;当下游取消收集时它被触发,执行清理代码块取消job1/job2,防止协程泄漏。
由于两个流是并发收集的,main 的输出顺序不固定,可能类似:
makefile
Flow1: 1
Flow2: A
Flow1: 2
Flow2: B
Flow1: 3
7. 总结
| 概念 | 作用 |
|---|---|
flowOn |
切换上游操作的协程上下文;不改变下游 |
FusibleFlow.fuse |
融合优化:多层上下文切换合并为一次,避免重复创建 Channel |
ChannelFlowOperatorImpl |
无法融合时的默认包装器,用 Channel 解耦上下游 |
collectTo |
上游在指定上下文收集并发送到 Channel;下游从 Channel 接收 |
ProducerScope |
生产者端协程作用域:既能 launch 子协程,又能 send/close Channel |
awaitClose |
保持 Channel 开放直到收集结束,并在取消时执行清理 |