- 一个典型的 Flow 调用链示例
<span>flowOf</span>(<span>1</span>, <span>2</span>, <span>3</span>)
<span>.map</span> { it * <span>2</span> }
<span>.filter</span> { it > <span>2</span> }
<span>.flowOn</span>(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来做线程切换——这正是后面几节要讲的内容。
- FusibleFlow:可融合 Flow 的内部接口
<span>internal</span> <span>interface</span> <span>FusibleFlow</span><<span>T</span>> : <span>Flow</span><<span>T</span>> {
<span><span>fun</span> <span>fuse</span><span>(context: <span>CoroutineContext</span>)</span></span>: Flow<T>?
}
描述:这是 kotlinx.coroutines 内部的接口(internal,不对使用者暴露)。它定义了一个 fuse 方法,表示"实现类知道如何把一个新的上下文融合进自己"。fuse 返回 Flow<T>?:
- 返回融合后的新 Flow(或自身)——融合成功;
- 返回
null—— 无法融合。
融合的意义:当多个 flowOn/操作符连续出现时,不必为每一次上下文切换都创建一个 Channel,而是把上下文合并到一起,只需最外层做一次真正的线程切换,大幅减少开销。
- flowOn 的源码实现
<span>public</span> <span><span>fun</span> <span><T></span> Flow<span><T></span>.<span>flowOn</span><span>(context: <span>CoroutineContext</span>)</span></span>: Flow<T> {
checkFlowContext(context)
<span>return</span> <span>when</span> {
context == EmptyCoroutineContext -> <span>this</span>
<span>this</span> <span>is</span> FusibleFlow -> fuse(context = context) <span>// 尝试融合,避免创建 Channel</span>
<span>else</span> -> ChannelFlowOperatorImpl(<span>this</span>, context = context) <span>// 无法融合,创建 ChannelFlow</span>
}
}
描述:flowOn 的核心逻辑只有三个分支:
-
checkFlowContext(context):校验传入上下文的合法性(例如不允许包含Job,否则会破坏 Flow 的结构化并发语义)。 -
context == EmptyCoroutineContext:传入的是空上下文,无需切换线程,直接返回原 Flow。 -
this is FusibleFlow:上游本身是可融合的 Flow(如经过flowOn/map等操作符包装后的对象),调用fuse尝试把新上下文融合进上游,避免再创建一层 Channel。 -
else:无法融合,包装成ChannelFlowOperatorImpl——通过 Channel 把上游发射与下游收集解耦,从而实现线程切换。 -
ChannelFlowOperatorImpl:包装器的简化结构
<span>// 简化后的源码结构示意</span>
<span>internal</span> <span>class</span> <span>ChannelFlowOperatorImpl</span><<span>T</span>>(
<span>private</span> <span>val</span> flow: Flow<T>, <span>// 上游 Flow</span>
<span>private</span> <span>val</span> context: CoroutineContext <span>// 需要切换到的协程上下文(例如 flowOn 指定的 Dispatcher)</span>
) : ChannelFlow<T>() {
<span>// ... 内部实现</span>
}
描述:当无法融合时,flowOn 会把上游 Flow 包成 ChannelFlowOperatorImpl。它持有两个关键成员:
flow:被包装的上游 Flow;context:flowOn指定的目标上下文(如Dispatchers.IO)。
它继承自 ChannelFlow<T>,核心思路是:上游在一个新协程里发射元素到 Channel,下游从 Channel 接收——Channel 就是线程切换的桥梁。
- collectTo:ChannelFlow 的核心收集逻辑
<span>override</span> <span>suspend</span> <span><span>fun</span> <span>collectTo</span><span>(scope: <span>ProducerScope</span><<span>T</span>>)</span></span> {
<span>// 1. 在指定的上下文中启动一个新的协程来收集上游 flow</span>
<span>// scope.coroutineContext 包含了 flowOn 指定的 dispatcher</span>
<span>val</span> newContext = scope.coroutineContext + context
<span>// 2. 启动子协程执行上游收集逻辑</span>
<span>// 注意:这里通常使用 scope.launch 或类似机制,确保生命周期绑定</span>
scope.launch(newContext) {
<span>try</span> {
<span>// 收集上游 flow,并将发射的元素发送到 channel (scope.send)</span>
flow.collect { value ->
scope.send(value)
}
} <span>catch</span> (e: Throwable) {
<span>// 异常处理:如果上游抛出异常,需要关闭 channel 并传播异常</span>
scope.close(e)
}
}
<span>// 3. 等待所有子协程完成,确保资源清理</span>
<span>// awaitClose 是 channelFlow/channelFlow 模式中的关键,用于保持 channel 开放直到逻辑结束</span>
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。
- ProducerScope:生产者作用域
上面 collectTo(scope: ProducerScope<T>) 中的 scope 就是 ProducerScope。它是 ChannelFlow 体系里的"生产者端协程作用域",定义大致如下:
<span>public</span> <span>interface</span> <span>ProducerScope</span><<span>T</span>> : <span>CoroutineScope</span> {
<span>val</span> channel: SendChannel<T> <span>// 生产者把元素发送进去的 Channel</span>
<span>suspend</span> <span><span>fun</span> <span>send</span><span>(value: <span>T</span>)</span></span> <span>// 便捷方法:等价于 channel.send(value)</span>
<span><span>fun</span> <span>close</span><span>(cause: <span>Throwable</span>? = <span>null</span>)</span></span> <span>// 关闭 Channel,可选地带上异常原因</span>
}
描述: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 作为接收者,可以最直观地看到它的两种身份如何配合使用:
<span>import</span> kotlinx.coroutines.flow.*
<span>import</span> kotlinx.coroutines.*
<span><span>fun</span> <span>mergeFlows</span><span>(flow1: <span>Flow</span><<span>Int</span>>, flow2: <span>Flow</span><<span>String</span>>)</span></span>: Flow<String> = channelFlow {
<span>// 身份 1:CoroutineScope —— 这里 this 就是 ProducerScope<String>,</span>
<span>// 所以可以直接 launch 子协程,作用域内调用 send(value) 不需要写 scope. 前缀</span>
<span>// 启动一个子协程收集 flow1 并发送</span>
<span>val</span> job1 = launch {
flow1.collect { value ->
send(<span>"Flow1: <span>$value</span>"</span>) <span>// 使用 ProducerScope 的 send 方法</span>
}
}
<span>// 启动另一个子协程收集 flow2 并发送</span>
<span>val</span> job2 = launch {
flow2.collect { value ->
send(<span>"Flow2: <span>$value</span>"</span>)
}
}
<span>// awaitClose 确保在所有子协程完成之前,Flow 保持活跃</span>
<span>// 当下游取消收集时,awaitClose 会触发,从而取消 job1 和 job2</span>
awaitClose {
job1.cancel()
job2.cancel()
}
}
<span>// 使用示例</span>
<span>suspend</span> <span><span>fun</span> <span>main</span><span>()</span></span> {
<span>val</span> flowA = flowOf(<span>1</span>, <span>2</span>, <span>3</span>)
<span>val</span> flowB = flowOf(<span>"A"</span>, <span>"B"</span>)
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 的输出顺序不固定,可能类似:
<span>Flow1: 1</span>
<span>Flow2: A</span>
<span>Flow1: 2</span>
<span>Flow2: B</span>
<span>Flow1: 3</span>
- 总结
| 概念 | 作用 |
|---|---|
| `flowOn` | 切换**上游**操作的协程上下文;不改变下游 |
| `FusibleFlow.fuse` | 融合优化:多层上下文切换合并为一次,避免重复创建 Channel |
| `ChannelFlowOperatorImpl` | 无法融合时的默认包装器,用 Channel 解耦上下游 |
| `collectTo` | 上游在指定上下文收集并发送到 Channel;下游从 Channel 接收 |
| `ProducerScope` | 生产者端协程作用域:既能 `launch` 子协程,又能 `send`/`close` Channel |
| `awaitClose` | 保持 Channel 开放直到收集结束,并在取消时执行清理 |
文章把 flowOn 的融合分支与 Channel 兜底讲得很透,适合想搞懂 Flow 线程切换开销、优化复杂操作符链的 Kotlin 开发者阅读,能直接指导 channelFlow 与 flowOn 的取舍。