FusibleFlow:Kotlin Flow 的融合机制原理

文章来源声明: 原文作者:hai_android; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684573646438842378; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

文章把 flowOn 的融合分支与 Channel 兜底讲得很透,适合想搞懂 Flow 线程切换开销、优化复杂操作符链的 Kotlin 开发者阅读,能直接指导 channelFlow 与 flowOn 的取舍。

> 本文以 `FusibleFlow` 为核心,通过源码片段解析 Flow 的"融合"(fuse)优化:`flowOn` 如何触发融合、`fuse` 如何把多层上下文合并为一次线程切换,以及无法融合时 `ChannelFlow` 兜底方案(`collectTo`)的工作方式。
  1. 一个典型的 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 收集到的就是 46。注意 flowOn 只影响上游操作符(flowOf/map/filter)的执行线程,不影响下游 collect 所在的协程。

对比:如果上游不是 FusibleFlow(例如一个自定义的、未实现该接口的 Flow),flowOn 就只能走 else 分支,创建 ChannelFlowOperatorImpl 来做线程切换——这正是后面几节要讲的内容。

  1. 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,而是把上下文合并到一起,只需最外层做一次真正的线程切换,大幅减少开销。

  1. 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 的核心逻辑只有三个分支:

  1. checkFlowContext(context) :校验传入上下文的合法性(例如不允许包含 Job,否则会破坏 Flow 的结构化并发语义)。

  2. context == EmptyCoroutineContext:传入的是空上下文,无需切换线程,直接返回原 Flow。

  3. this is FusibleFlow:上游本身是可融合的 Flow(如经过 flowOn/map 等操作符包装后的对象),调用 fuse 尝试把新上下文融合进上游,避免再创建一层 Channel。

  4. else:无法融合,包装成 ChannelFlowOperatorImpl——通过 Channel 把上游发射与下游收集解耦,从而实现线程切换。

  5. 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
  • contextflowOn 指定的目标上下文(如 Dispatchers.IO)。

它继承自 ChannelFlow<T>,核心思路是:上游在一个新协程里发射元素到 Channel,下游从 Channel 接收——Channel 就是线程切换的桥梁。

  1. 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()
}

描述collectToChannelFlow 真正干活的地方,分三步:

  1. 合并上下文scope.coroutineContext + contextflowOn 指定的上下文叠加到生产者协程上下文之上,得到 newContext——上游收集将在这个上下文中执行,这就是线程切换的落点
  2. 启动子协程收集上游:在 newContextlaunch 一个子协程,收集上游 flow,并把每个发射的元素通过 scope.send(value) 写入 Channel;若上游抛异常,则 scope.close(e) 关闭 Channel 并把异常传播给下游。
  3. awaitClose 挂起等待:保持 Channel 开放,直到所有子协程结束或下游取消收集。它保证了生产者的生命周期与 Flow 的收集生命周期绑定,防止资源泄漏。

小结flowOn 的本质 = 上游在指定上下文的协程中发射 → 经 Channel 传递 → 下游在收集者上下文中接收。而"融合"优化是在有多层 flowOn/操作符时省掉多余的 Channel。

  1. 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 切换机制的原因:

  1. CoroutineScope(协程作用域) :所以才能在 collectTo 里直接调用 scope.launch(newContext) 启动子协程、通过 scope.coroutineContext 读取上下文。子协程自动挂在这个作用域下,随 Flow 收集的取消而一起取消,实现生命周期绑定
  2. Channel 的"发送端"(SendChannelscope.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 可以直接调用——job1job2 两个子协程并发收集 flow1flow2。这也是 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>

  1. 总结

概念作用
`flowOn`切换**上游**操作的协程上下文;不改变下游
`FusibleFlow.fuse`融合优化:多层上下文切换合并为一次,避免重复创建 Channel
`ChannelFlowOperatorImpl`无法融合时的默认包装器,用 Channel 解耦上下游
`collectTo`上游在指定上下文收集并发送到 Channel;下游从 Channel 接收
`ProducerScope`生产者端协程作用域:既能 `launch` 子协程,又能 `send`/`close` Channel
`awaitClose`保持 Channel 开放直到收集结束,并在取消时执行清理