Flow 性能:16. 流的默认执行性能分析

本篇解析 example-flow-16.kt。探讨在没有优化的情况下,Flow 的执行效率问题。

1. 核心问题:串行执行的开销

默认情况下,Flow 的发射和收集是串行的。

  • 发射端:每 100ms 产出一个。
  • 收集端:每处理一个需要 300ms。

执行时间计算:

由于是串行,处理一个元素的总耗时 = 发射耗时 + 收集耗时 = 100ms + 300ms = 400ms。
处理 3 个元素总共需要:400ms * 3 = 1200ms

2. 开发者感悟

在数据量较小或两端速度都很快时,这种串行模式没有问题。但如果收集端涉及复杂的 UI 渲染或 IO 写入,而发射端又是高频产生的,这种串行模式就会导致严重的性能瓶颈。我们需要一种“并行”机制。

Flow 性能:17. 使用 Buffer 优化性能

本篇解析 example-flow-17.kt。学习如何通过缓冲机制实现并发执行。

1. 核心操作符:buffer()

buffer() 会在发射端和收集端之间创建一个缓冲区。

工作原理:

  • 并发执行:发射端不再等待收集端处理完,而是持续产出并放入缓冲区。收集端则不断从缓冲区拿数据处理。
  • 底层实现:它会在后台自动切换协程,类似于 flowOn

2. 性能提升分析

  • 发射时间:3 * 100ms = 300ms。
  • 收集时间:3 * 300ms = 900ms。
  • 由于并发:收集端在等待第一个元素时,发射端已经开始准备第二个了。
  • 总耗时:第一个元素产生 (100ms) + 收集 3 个元素 (900ms) ≈ 1000ms
  • 对比之前的 1200ms,节省了明显的等待时间。

3. 开发者感悟

buffer 是提升 Flow 处理效率的利器。如果你的数据生产和消费都很耗时,加上 buffer() 能显著减少用户的整体等待时间。

Flow 性能:18. Conflate 合并最新值

本篇解析 example-flow-18.kt。学习如何处理消费能力严重不足的情况。

1. 核心操作符:conflate()

当收集器处理速度远慢于发射速度时,如果你不关心中间值,只想要最新的数据,可以使用 conflate()

工作原理:

  • 跳过中间值:当收集器正在忙于处理一个元素时,如果发射端又产出了多个新值,conflate 会直接丢弃旧的中间值,只保留最新的那一个。
  • 不阻塞发射:发射端可以一直运行,不需要等待收集端。

2. 代码解析

1
2
3
4
5
6
simple()
.conflate()
.collect { value ->
delay(300) // 耗时 300ms
println(value)
}
  • 100ms:发射 1,收集端开始处理 1(耗时 300ms)。
  • 200ms:发射 2,由于收集端在忙,2 被放入暂存。
  • 300ms:发射 3,由于收集端还在忙,2 被丢弃,3 替代 2 成为最新值。
  • 400ms:收集端处理完 1,立即开始处理最新的 3。
  • 结果:只打印了 1 和 3,中间的 2 被“合并”掉了。

3. 开发者感悟

conflate 非常适合处理高频更新的 UI 状态。例如:股票行情或进度条更新。用户不需要看到每一个细微的数字变化,他们只需要看到当前的最新状态即可。这能极大减轻 UI 线程的压力。

Flow 性能:19. CollectLatest 处理最新值

本篇解析 example-flow-19.kt。学习一种处理消费慢的进阶手段。

1. 核心操作符:collectLatest

collectLatestconflate 类似,都关注“最新值”。但它们的处理逻辑不同:

  • conflate:如果收集器忙,就丢弃发射出的中间值。
  • collectLatest:当新值发射时,如果收集器正在处理旧值,它会立即取消旧值的处理代码块,并开始处理新值。

2. 代码解析

1
2
3
4
5
6
simple()
.collectLatest { value ->
println("Collecting $value")
delay(300) // 模拟 300ms 的处理逻辑
println("Done $value")
}
  • 100ms:发射 1,开始处理 1。
  • 200ms:发射 2。此时 1 仅处理了 100ms,立即被取消,开始处理 2。
  • 300ms:发射 3。此时 2 仅处理了 100ms,立即被取消,开始处理 3。
  • 600ms:3 终于处理完了(300ms),打印 “Done 3”。
  • 结果:只看到 1, 2, 3 的 “Collecting” 日志,但只有 3 打印了 “Done”。

3. 开发者感悟

collectLatest 非常适合处理诸如“搜索框自动补全”的场景。当用户输入下一个字母时,上一个字母对应的搜索请求已经没意义了,直接取消掉,只处理最后一次输入的请求。这能极大节省网络和计算资源。

Flow 组合:20. Zip 组合两个流

本篇解析 example-flow-20.kt。学习如何将两个独立的数据源进行配对组合。

1. 核心操作符:zip

zip 操作符用于将两个流中的对应元素组合成一个新的元素。

特点:

  • 一一对应:第一个流的第 N 个元素只会与第二个流的第 N 个元素配对。
  • 短板效应:如果其中一个流结束了,整个 zip 后的流也会立即结束。

2. 代码解析

1
2
3
4
val nums = (1..3).asFlow() // 1, 2, 3
val strs = flowOf("one", "two", "three") // "one", "two", "three"
nums.zip(strs) { a, b -> "$a -> $b" }
.collect { println(it) }
  • 输出
    1 -> one
    2 -> two
    3 -> three

3. 开发者感悟

zip 在处理“多源同步”时非常有用。例如:你需要同时请求用户的个人资料和订单列表,并把这两者合并成一个 UIModel。只有当两个请求都成功返回对应的结果时,zip 才会产出合并后的对象。

Flow 组合:21. Zip 同步等待机制

本篇解析 example-flow-21.kt。探讨 zip 在处理不同速流时的行为。

1. 核心概念:等待最慢的那一个

zip 组合时,每个元素都必须成对出现。如果一个流快,另一个流慢,快的那个流必须等待慢的流产出对应的元素。

2. 代码解析

1
2
3
4
5
6
val nums = (1..3).asFlow().onEach { delay(300) } // 每 300ms 产出一个
val strs = flowOf("one", "two", "three").onEach { delay(400) } // 每 400ms 产出一个

nums.zip(strs) { a, b -> "$a -> $b" }.collect { value ->
println("$value at ${currentTimeMillis() - startTime} ms")
}
  • 结果分析
    1. 第一对 (1, one):在 400ms 后产出(等待 strs)。
    2. 第二对 (2, two):在 800ms 后产出(累加等待)。
    3. 第三对 (3, three):在 1200ms 后产出。

3. 开发者感悟

zip 具有天然的同步属性。它就像是齿轮咬合:只有两个齿轮都转到对应位置,这一格才算走完。如果你的业务场景允许两端流“各自跑,谁快谁更新”,那么你应该使用 combine 而不是 zip

Flow 组合:22. Combine 组合最新值

本篇解析 example-flow-22.kt。学习一种比 zip 更灵活的流组合方式。

1. 核心操作符:combine

combine 也用于组合两个流,但它不要求元素一一对应。

工作原理:

  • 最新值组合:只要其中任何一个流发射了新值,combine 就会取两个流中各自最新的值进行组合并发射。
  • 不等待同步:快流不需要等待慢流。

2. 代码解析

1
2
3
4
val nums = (1..3).asFlow().onEach { delay(300) } // 每 300ms 产出一个
val strs = flowOf("one", "two", "three").onEach { delay(400) } // 每 400ms 产出一个

nums.combine(strs) { a, b -> "$a -> $b" }.collect { ... }
  • 结果分析
    1. 400ms:strs 产出 “one”,此时 nums 最新值是 1 -> 输出 1 -> one
    2. 600ms:nums 产出 2,此时 strs 最新值仍是 “one” -> 输出 2 -> one
    3. 800ms:strs 产出 “two”,此时 nums 最新值是 2 -> 输出 2 -> two
    4. … 依此类推。

3. 开发者感悟

combine 在 UI 开发中极其常用。例如:你的界面有两个输入框(用户名和密码),只有当两个输入框都校验通过时,登录按钮才可用。你可以将两个输入框的 TextFlow 进行 combine,任何一个变化都会触发按钮状态的重新计算。

Flow 组合:23. FlatMapConcat 顺序平坦化

本篇解析 example-flow-23.kt。学习如何处理“流中流”的嵌套情况。

1. 核心操作符:flatMapConcat

当你有一个初始流,且它的每个元素都会触发另一个流(子流)时,你会得到一个 Flow<Flow<T>>flatMapConcat 的作用就是将这些子流按顺序连接并铺平。

特点:

  • 顺序执行:它会等待第一个子流完全发射完毕后,才开始处理第二个子流。
  • 等价于map { requestFlow(it) }.flattenConcat()

2. 代码解析

1
2
3
(1..3).asFlow()
.flatMapConcat { requestFlow(it) } // 请求 1 结束后才开始请求 2
.collect { println(it) }
  • 结果分析
    1. 1: First (100ms) -> 1: Second (600ms)。
    2. 2: First (700ms) -> 2: Second (1200ms)。
    3. … 严格保证了 1, 2, 3 的顺序。

3. 开发者感悟

flatMapConcat 就像是串行的网络请求。例如:你需要先通过用户名查到用户 ID(子流 1),拿到 ID 后再去查用户的详细信息(子流 2)。它保证了任务的逻辑顺序。

Flow 组合:24. FlatMapMerge 并发平坦化

本篇解析 example-flow-24.kt。学习如何并发地处理多个子流。

1. 核心操作符:flatMapMerge

flatMapConcat 不同,flatMapMerge并发地订阅并处理所有的子流。

特点:

  • 非顺序性:它不会等待上一个流结束才开始下一个。这意味着不同子流的发射值可能会交织在一起。
  • 高效性:它能显著提高处理速度,因为多个子流是并发运行的。
  • 并发限制:可以通过参数 concurrency 限制同时运行的子流数量(默认值为 16)。

2. 代码解析

1
2
3
(1..3).asFlow()
.flatMapMerge { requestFlow(it) } // 同时发起 1, 2, 3 的请求
.collect { println(it) }
  • 结果分析
    1. 1: First, 2: First, 3: First (几乎同时产出)。
    2. 1: Second, 2: Second, 3: Second (500ms 后几乎同时产出)。
  • 总耗时:约为单次请求的时间(600ms 左右),而不是三次串行的时间。

3. 开发者感悟

flatMapMerge 是处理“多任务并行”的神器。例如:你需要同时根据 10 个 ID 获取对应的详情数据。如果你不在乎数据的先后顺序,只在乎总时间越短越好,那么它是你的不二之选。

Flow 组合:25. FlatMapLatest 处理最新流

本篇解析 example-flow-25.kt。学习一种极具响应性的嵌套流处理方式。

1. 核心操作符:flatMapLatest

flatMapLatestcollectLatest 的设计理念一致:只对最新的发射值感兴趣

工作原理:

  • 切换与取消:每当初始流发射一个新值时,如果上一个子流(由 flatMapLatest 产生)还在运行,它会立即取消旧的子流,并开启新的子流。

2. 代码解析

1
2
3
(1..3).asFlow().onEach { delay(100) } // 每 100ms 发射一个数字
.flatMapLatest { requestFlow(it) } // 启动一个耗时 500ms 的子流
.collect { println(it) }
  • 结果分析
    1. 100ms:发射 1,启动子流 1 -> 产出 1: First
    2. 200ms:发射 2。此时子流 1 还在 delay立即被取消,启动子流 2 -> 产出 2: First
    3. 300ms:发射 3。此时子流 2 还在 delay立即被取消,启动子流 3 -> 产出 3: First
    4. 800ms:子流 3 终于跑完了 500ms 的 delay -> 产出 3: Second
  • 输出1: First, 2: First, 3: First, 3: Second。只有最后一个子流完成了。

3. 开发者感悟

flatMapLatest 是处理诸如“即时搜索”的最佳方案。用户每输入一个字,我们就取消上一次的 API 请求流,开始新的请求流。这能保证 UI 始终反映用户的最后一次操作。