1) Flow 内部大模型(先有这个心智图)
-
冷流:每次 collect 都从头再建一条“处理链”。
-
链式组装:绝大多数中间运算符(map/filter/transform/onEach/scan/distinctUntilChanged…)都是 inline 的函数,返回一个新的 Flow,其 collect 实现里会 把上游的 emit 直接转发到下游的 emit,中间仅加上自己的那点逻辑。
-
单协程、串行直通(默认):没有 buffer/flowOn/flatMap* 等“并发型运算符”时,整条链在 同一个协程同一线程 上 逐元素 运行,上游 emit 一次就调用一串内联的操作,然后到达下游 emit。
-
边界(Boundary) :遇到 buffer/flowOn/flatMapMerge/collectLatest/channelFlow/zip/combine/shareIn 等,会引入 额外协程 + Channel,把“直通链”切成多个段。
直观理解:能“直通”的都尽量直通;一旦需要并发/上下文切换/多路合流,就建 ChannelFlow + 协程 做解耦。
2) 什么是运算符融合(Operator Fusion)
定义:把一串同步、无并发、无上下文切换的中间运算符,在一次元素处理里连成一条内联调用链,避免为每个运算符单独创建协程/队列/对象,从而降低开销(没有额外调度、无多余分配)。
简化到源码层面的典型实现(伪代码):
inline fun <T, R> Flow<T>.map(crossinline transform: suspend (T) -> R): Flow<R> =
unsafeFlow { downstream ->
collect { value -> // 上游 emit(value)
downstream.emit(transform(value)) // 直接算、直接发下游
}
}
filter、onEach、scan 等都类似:一层套一层的内联——这就是“融合”。
收益:
- 无额外协程/Channel;
- 无多余上下文切换;
- 极少分配(大多为已内联的 lambda + 编译期状态机)。
3) 哪些会被融合?(按“是否引入并发/上下文”来记)
完全可融合(同步串行) :
-
map / mapNotNull / filter / take / drop / onEach / transform(只要不内部 launch)
-
scan / runningFold / distinctUntilChanged
-
catch / onCompletion / retry(不制造并发,包裹在同一链路上)
-
debounce(注意:实现包含时间器,但不拆分上下文时仍在同一收集协程里调度,通常与链路融合)
部分融合(逻辑在一侧融合,但会与另一侧形成边界) :
-
flowOn:在 flowOn 上游 的运算符彼此融合;flowOn 自身是边界(见下)。
-
takeWhile / transformWhile:本身融合,但提前终止整条链。
几乎必定打破融合(构造边界) :
-
buffer(n) / conflate() / collectLatest()
→ 构造 ChannelFlow:上游生产、下游消费分离,出现队列与独立协程。
-
flatMapMerge/Concat/Latest、mapLatest
→ 每个元素“展开”出子协程(latest 会取消老子协程),天然并发边界。
-
flowOn(context)
→ 切换到另一个 dispatcher,强边界;flowOn 上下游各自内部依旧融合。
-
zip / combine
→ 需要多源对齐/合并,内部必有协调与缓存。
-
shareIn / stateIn(热流共享)
→ 生成上游生产协程 + 共享状态/重放缓存,下游再各自收集。
口诀:不引入并发和上下文切换 → 融合;只要“跨协程/跨上下文/多路合流” → 边界。****
4) 融合后的性能画像(每个元素会发生什么)
在无边界链路中,处理一个元素的路径类似:
upstream.emit(v)
→ (map λ) → (filter 判定) → (onEach 副作用) → downstream.emit(v’)
- 无多余调度:不切线程,不入队/出队。
- 挂起点:只有当你的 transform/emit 自身挂起时才会挂起(比如写入某个挂起 API)。纯同步运算几乎就是函数内联调用的成本。
- SafeCollector 校验:为了上下文一致性/正确的 flowOn 语义,emit 路上有一次上下文一致性检查(线程局部开销很小,通常可忽略;flowOn 边界会换一套 SafeCollector)。
5) 边界一旦出现,会带来什么?
以 buffer(n) 为例:
-
新增协程:上游在 A 协程生产、下游在 B 协程消费;
-
新增队列:Channel(capacity=n);
-
收益:抗抖/解耦(上游快、下游慢时不互相阻塞);
-
代价:入队/出队 + 调度切换 + 可能的元素缓存(内存占用);丢失“每元素严格顺序的同线程直通”。
因此:
- 只在确实需要解耦时加 buffer;
- conflate 会保留最新一条,丢弃中间(适合“状态类”数据);
- collectLatest/mapLatest/flatMapLatest 通过取消旧工作换来“只处理最新”。
6) 与flowOn的融合边界
flowOn(Dispatchers.IO) 会把它之前的运算放到 IO 线程跑、之后的运算继续在原上下文(常是 Main)。
-
flowOn 之前的同步运算符互相融合;之后的一组也融合;
-
flowOn 本身是强边界,内部用 ChannelFlow/调度器将两段链路隔离。
实战要点:
- 把 flowOn 靠近源头(I/O 转换在 IO、UI 变换在 Main),减少跨线程往返;
- 同理,把 buffer 放在瓶颈前,只按需放 1~2 处即可。
7) 与 RxJava“融合”的对照
- RxJava 有**同步/异步融合(fusion)**协议(QueueFuseable),能把某些 map/filter 合并为一次拉取;
- Kotlin Flow 的“融合”更多是编译期内联 + 单协程直通,不是在运行期协商协议位,而是通过不创建并发边界来达到“融合”的效果。
- 共同目标相同:减少切换与中间层,尽量把一串轻运算一次做完。
8) 实战优化清单(按“最常见收益”排序)
- 优先保持“直通链” :能不 buffer/flowOn/flatMap* 就不要;确实需要才加(并只加一次)。
- 把 flowOn 靠近源头:I/O 相关运算放 flowOn(IO) 之前;UI 侧变换放之后。
- 把多个轻运算合并为一个 transform{} (可读性允许时):
flow.transform { v ->
if (predicate(v)) emit(f(v)) // 相当于 filter + map 的融合版
}
-
用 mapLatest/collectLatest 处理“只要最新”场景,少自己写“取消上一份”的样板;
但要知道它会打破直通(每元素子协程),请只在必要时用。
-
避免无意义的 flowOn/buffer 叠加:每加一次就是一个边界;能合并的合并。
-
状态流用 conflate/distinctUntilChanged:减少冗余运算与 UI 刷新。
-
组合类运算符知其代价:zip/combine 必有协调缓存;确定需要再用,避免“大杂烩链路”。
-
测量而非猜测:配合 DebugProbes/日志标注关键节点;观察线程与是否出现“边界”(是否出现额外协程/队列)。
9) 误解澄清
- “多个 map 就很慢” :只要没有边界,它们会被内联融合,通常不是瓶颈;真正的开销来自跨协程/队列/线程。
- “flowOn 一定切线程两次” :不是两次,是将边界前的运算放到指定 dispatcher;之后继续在下游上下文;关键是放的位置。
- “buffer 越大越好” :不是。大缓冲 = 更大内存 + 更难掌控时序;按需要设置小容量(如 0/1/固定小数)。
10) 速查表
- 可融合:map/filter/transform/onEach/scan/distinctUntilChanged/take/drop/catch/onCompletion/retry
- 边界:buffer/conflate/collectLatest/flowOn/flatMap*/mapLatest/channelFlow/zip/combine/shareIn/stateIn
- 放置:flowOn 靠近源;buffer 放瓶颈前;边界越少越快。
- 只要最新:用 *Latest 或 conflate。
- 合并轻运算:必要时用 transform{} 同步融合。