Kotlin协程Flow及管道中的buffer和bufferCapacity

Kotlin协程Flow及管道中的buffer和bufferCapacity

mChannel.receiveAsFlow()

.onEach {

// do something

}

.buffer(bufferCapacity)

其中:

bufferCapacity = 10

这里的 bufferCapacity = 10 表示的是:

在 Flow 管道中,onEach 上游和下游之间增加一个容量为 10 的缓冲区。

它不是 mChannel 本身的容量,也不是整个系统最多只能缓存 10 个元素。

1. 先理解 Channel 和 receiveAsFlow()

假设有一个 Channel:

val mChannel = Channel<Int>()

然后:

mChannel.receiveAsFlow()

表示把这个 Channel 转成一个 Flow。也就是说,后面可以用 Flow 的操作符来处理 Channel 中收到的数据:

mChannel.receiveAsFlow()

.onEach {

println("receive: $it")

}

.collect {

println("collect: $it")

}

需要注意:

receiveAsFlow()

本质上还是在从 mChannel 中接收数据。

它不是把 Channel 复制了一份,也不是广播。

如果多个地方同时 collect 这个 Flow,那么多个 collector 会竞争消费同一个 Channel 中的数据。

2. buffer(10) 的核心意义

mChannel.receiveAsFlow()

.onEach {

// do something

}

.buffer(10)

可以理解为:

mChannel

-> receiveAsFlow()

-> onEach { ... }

-> 容量为 10 的 Flow 内部缓冲区

-> 下游操作符 / collect

也就是说,buffer(10) 会在 Flow 管道中创建一个内部缓冲区,最多可以暂存 10 个元素。

3. 它不是限制 mChannel 的容量

这是第一个重点。

.buffer(10)

限制的是 Flow 内部某一段管道之间的缓冲容量。

它并不会把原来的 mChannel 变成容量为 10 的 Channel。

例如:

val mChannel = Channel<Int>(Channel.UNLIMITED)

mChannel.receiveAsFlow()

.onEach { println(it) }

.buffer(10)

.collect { delay(1000) }

这里虽然 Flow 上写了:

.buffer(10)

但如果 mChannel 本身是 Channel.UNLIMITED,生产者还是可能不断往 mChannel 里发送数据,最终导致内存压力。

所以:

buffer(10) 不等于系统最多积压 10 条消息。

真正的积压可能来自:

  1. 原始 mChannel 自己的容量;
  2. Flow.buffer(10) 的容量;
  3. 下游正在处理但还未完成的元素;
  4. 其他业务层缓存。

4. buffer(10) 会让上下游并发执行

没有 buffer 的时候,Flow 通常是顺序执行的。

例如:

mChannel.receiveAsFlow()

.onEach {

println("onEach: $it")

}

.collect {

delay(1000)

println("collect: $it")

}

大致执行方式是:

收到 1

执行 onEach 1

执行 collect 1,耗时 1 秒

收到 2

执行 onEach 2

执行 collect 2,耗时 1 秒

收到 3

执行 onEach 3

执行 collect 3,耗时 1 秒

上游和下游基本是串行的。

但是加了:

.buffer(10)

之后:

mChannel.receiveAsFlow()

.onEach {

println("onEach: $it")

}

.buffer(10)

.collect {

delay(1000)

println("collect: $it")

}

它更像是:

val internalBuffer = Channel<Int>(capacity = 10)

launch {

for (item in mChannel) {

println("onEach: $item")

internalBuffer.send(item)

}

}

launch {

for (item in internalBuffer) {

delay(1000)

println("collect: $item")

}

}

也就是说,buffer(10) 会让它前面的部分和后面的部分可以并发运行。

5. buffer(10) 的实际效果

假设下游处理很慢:

mChannel.receiveAsFlow()

.onEach {

println("onEach: $it")

}

.buffer(10)

.collect {

delay(1000)

println("collect: $it")

}

如果 mChannel 中很快来了很多数据:

1, 2, 3, 4, 5, ... 100

那么上游可能会先快速执行:

onEach: 1

onEach: 2

onEach: 3

...

onEach: 10

这些元素会进入 buffer(10) 的内部缓冲区。

下游 collect 则慢慢处理:

collect: 1

1 秒后

collect: 2

1 秒后

collect: 3

当内部缓冲区满了,比如已经缓存了 10 个元素,而下游还没消费掉,那么上游就会被挂起。

所以默认情况下:

.buffer(10)

的行为是:

最多缓存 10 个元素;满了之后,上游挂起等待下游消费。

6. 默认溢出策略是挂起,不是丢弃

buffer 的完整形式大概是:

.buffer(

capacity = 10,

onBufferOverflow = BufferOverflow.SUSPEND

)

也就是说,默认是:

BufferOverflow.SUSPEND

当缓冲区满了,上游会暂停,而不是丢弃数据。

例如:

.buffer(10)

等价理解为:

Kotlin

.buffer(

capacity = 10,

onBufferOverflow = BufferOverflow.SUSPEND

)

所以如果没有显式指定丢弃策略,数据不会因为 buffer 满了就自动丢掉,而是上游会被反压。

7. bufferCapacity = 10 不是 batch size

这个也很容易误解。

.buffer(10)

不是说每 10 个元素处理一次。

它不是批处理。

它只是说:

上游最多可以比下游提前生产 10 个元素。

如果想每 10 个元素组成一批处理,那需要自己做批量逻辑,比如:

chunked

或者手写收集逻辑,而不是用 buffer(10)。

8. 这个位置的 buffer(10) 特别关键

代码是:

mChannel.receiveAsFlow()

.onEach {

// do something

}

.buffer(10)

注意,buffer(10) 在 onEach 后面。

这意味着:

onEach { ... }

属于 buffer 的上游。

也就是说,onEach 里面的代码可能会先执行,并且最多领先下游大约 10 个元素。

例如:

mChannel.receiveAsFlow()

.onEach {

println("onEach: $it")

}

.buffer(10)

.collect {

delay(1000)

println("collect: $it")

}

可能出现:

onEach: 1

onEach: 2

onEach: 3

onEach: 4

onEach: 5

onEach: 6

onEach: 7

onEach: 8

onEach: 9

onEach: 10

collect: 1

collect: 2

...

所以如果在 onEach 里面做的是:

  1. 日志打印;
  2. 埋点;
  3. 状态修改;
  4. 数据库写入;
  5. 网络请求;
  6. 业务副作用;

那要非常小心。

因为它可能代表的是:

数据已经从 Channel 中取出来,并且进入 Flow buffer 了。

但并不代表:

下游已经真正处理完成了。

9. 如果副作用必须和最终消费绑定,别放在 buffer 前面

例如这样写:

mChannel.receiveAsFlow()

.onEach {

markAsProcessing(it)

}

.buffer(10)

.collect {

process(it)

}

这里 markAsProcessing(it) 可能比 process(it) 提前执行很多。

如果程序在中间取消了,可能出现:

已经 markAsProcessing

但还没有真正 process

这在任务队列、订单处理、消息确认场景中很危险。

更安全的写法可能是:

mChannel.receiveAsFlow()

.buffer(10)

.collect {

markAsProcessing(it)

process(it)

}

或者:

mChannel.receiveAsFlow()

.buffer(10)

.onEach {

markAsProcessing(it)

}

.collect {

process(it)

}

但具体放在哪里,要看具体业务。

重点是:

buffer 前面的逻辑表示"进入缓冲区之前执行",buffer 后面的逻辑表示"从缓冲区取出之后执行"。

10. buffer(10) 会影响线程吗?

buffer 会引入协程边界,让上下游可以在不同协程中执行。

但它不一定意味着一定切换到不同线程。

例如:

mChannel.receiveAsFlow()

.onEach {

println("onEach thread = ${Thread.currentThread().name}")

}

.buffer(10)

.collect {

println("collect thread = ${Thread.currentThread().name}")

}

可能看到相同线程,也可能看到不同线程,取决于:

  1. 当前协程的 Dispatcher;
  2. 是否使用了 flowOn;
  3. 是否在 Dispatchers.Default、Dispatchers.IO 等线程池上;
  4. 调度器当时如何调度。

所以:

buffer(10)

主要改变的是协程执行结构和背压关系,不是直接指定线程。

如果想明确改变上游执行线程,需要用:

.flowOn(Dispatchers.IO)

例如:

mChannel.receiveAsFlow()

.onEach {

println("onEach: ${Thread.currentThread().name}")

}

.flowOn(Dispatchers.IO)

.buffer(10)

.collect {

println("collect: ${Thread.currentThread().name}")

}

不过 flowOn 和 buffer 的位置也会影响执行边界,实际项目中要谨慎设计。

11. 和 Channel 自身容量的关系

这是实际开发中最重要的点之一。

假设:

val mChannel = Channel<Int>(capacity = 100)

然后:

mChannel.receiveAsFlow()

.onEach { }

.buffer(10)

.collect { }

那么系统中可能积压的数据并不是 10 个,而是:

mChannel 自身最多 100 个

Flow buffer 最多 10 个

正在被处理的元素

所以总积压可能超过 110。

如果:

val mChannel = Channel<Int>(Channel.UNLIMITED)

那么即使 Flow buffer 是 10,也无法防止 mChannel 本身无限积压。

所以如果想控制整体内存,一定要控制源头:

val mChannel = Channel<Int>(capacity = 100)

而不是只依赖:

.buffer(10)

12. 不同 Channel 容量下的效果

12.1 Rendezvous Channel

val mChannel = Channel<Int>()

默认是容量为 0 的 Rendezvous Channel。

发送方 send 和接收方 receive 要配对。

但是如果后面加了:

mChannel.receiveAsFlow()

.buffer(10)

.collect { delay(1000) }

那么 Flow 会从 mChannel 中提前接收一些数据放入 buffer。

这会让发送方看起来可以连续发送大约 10 个元素,而不必每个都等下游处理完成。

12.2 Bounded Channel

val mChannel = Channel<Int>(capacity = 100)

再加:

.buffer(10)

意味着整体缓冲能力更强。

Channel 里面可以积压 100 个

Flow buffer 里面可以积压 10 个

这种情况吞吐可能更好,但延迟和内存占用也可能增加。

12.3 Unlimited Channel

val mChannel = Channel<Int>(Channel.UNLIMITED)

这种情况下:

.buffer(10)

对防止内存无限增长帮助不大。

因为生产者可以先把大量数据塞进 mChannel。

如果生产速度长期大于消费速度,仍然可能导致内存问题。

13. 实际开发中的注意事项

13.1 不要把 buffer(10) 当成整体容量限制

错误理解:

mChannel.receiveAsFlow()

.buffer(10)

以为最多积压 10 个任务。

正确理解:

只是 Flow 管道中某一段最多缓冲 10 个元素。

如果 mChannel 本身有容量,或者是无限容量,总积压可能远超过 10。

13.2 注意 buffer 的位置

下面两段代码语义不同。

第一种:

mChannel.receiveAsFlow()

.onEach {

println("onEach before buffer: $it")

}

.buffer(10)

.collect {

delay(1000)

println("collect: $it")

}

onEach 在 buffer 前面,可能提前执行。

第二种:

Kotlin

mChannel.receiveAsFlow()

.buffer(10)

.onEach {

println("onEach after buffer: $it")

}

.collect {

delay(1000)

println("collect: $it")

}

onEach 在 buffer 后面,只有下游从 buffer 中取出元素后才执行。

所以:

buffer 的位置决定了哪些逻辑可以提前执行,哪些逻辑必须跟随下游消费节奏。

13.3 小心副作用提前执行

例如:

Kotlin

mChannel.receiveAsFlow()

.onEach {

updateDbStatus(it.id, "PROCESSING")

}

.buffer(10)

.collect {

reallyProcess(it)

}

如果 reallyProcess 很慢,updateDbStatus 可能提前对 10 个任务执行。

如果中途协程取消,可能导致状态不一致。

任务处理类业务中,建议把关键副作用放在真正处理逻辑附近。

13.4 如果下游很慢,buffer 只能缓解,不能解决根因

例如:

mChannel.receiveAsFlow()

.buffer(10)

.collect {

delay(5000)

process(it)

}

如果生产者每秒发送 1000 条,而消费者 5 秒处理 1 条,那么 buffer(10) 只能短暂缓冲。

最终还是会:

  1. 上游挂起;
  2. Channel 积压;
  3. 内存上涨;
  4. 延迟越来越高。

这时候需要考虑:

  1. 增加消费者并发;
  2. 限流;
  3. 丢弃策略;
  4. 合并请求;
  5. 使用背压;
  6. 控制 Channel 容量;
  7. 监控队列长度。

13.5 默认不会丢数据,但可能导致上游挂起

.buffer(10)

默认是挂起策略:

BufferOverflow.SUSPEND

如果不希望上游挂起,而是希望丢弃,可以显式写:

.buffer(

capacity = 10,

onBufferOverflow = BufferOverflow.DROP_OLDEST

)

或者:

.buffer(

capacity = 10,

onBufferOverflow = BufferOverflow.DROP_LATEST

)

例如 UI 场景中,只关心最新状态,可以考虑丢弃旧值。

但是任务处理、订单处理、支付消息这类场景一般不能随便丢。

13.6 不要随便用很大的 buffer

例如:

.buffer(10_000)

这可能提高短时间吞吐,但也可能带来问题:

  1. 内存占用增加;
  2. 下游延迟变大;
  3. 取消时未处理数据更多;
  4. 问题被隐藏,生产消费不平衡更晚暴露;
  5. OOM 风险增加。

buffer 不是越大越好。

合理做法是根据业务选择:

.buffer(10)

.buffer(64)

.buffer(100)

然后结合压测和监控调整。

13.7 receiveAsFlow() 不是广播

如果你需要广播,让多个订阅者都收到同一份数据,应该考虑:

SharedFlow

或者:

StateFlow

而不是 Channel.receiveAsFlow()。

13.8 注意取消和关闭

Channel 转成 Flow 后,仍然要关注生命周期。

比如:

mChannel.close()

正常关闭后,receiveAsFlow() 对应的 Flow 会结束。

如果 Channel 是异常关闭:

mChannel.close(cause)

那么 Flow 收集时也可能收到异常。

实际开发中要注意:

try {

mChannel.receiveAsFlow()

.buffer(10)

.collect {

process(it)

}

} catch (e: Throwable) {

// handle error

}

同时,不要只取消 collector,却忘了生产者还在不断 send。

否则可能造成生产端挂起或者资源泄漏。

14. 一个例子:观察 buffer 的效果

val mChannel = Channel<Int>()

launch {

repeat(100) {

println("send: $it")

mChannel.send(it)

}

}

mChannel.receiveAsFlow()

.onEach {

println("onEach: it, thread={Thread.currentThread().name}")

}

.buffer(10)

.collect {

delay(1000)

println("collect: it, thread={Thread.currentThread().name}")

}

可能会看到 onEach 先快速打印多个元素,而 collect 慢慢打印。

原因就是:

.buffer(10)

允许上游比下游最多提前大约 10 个元素。

15. 如果希望限制整个系统最多 10 个任务,应该怎么做?

不要只写:

mChannel.receiveAsFlow()

.buffer(10)

更应该从源头控制:

val mChannel = Channel<Task>(capacity = 10)

然后根据需要决定 Flow 里面是否还要 buffer:

mChannel.receiveAsFlow()

.collect {

process(it)

}

如果已经在 Channel 层做了容量限制,Flow 里是否还需要 buffer 要谨慎评估。

因为:

Channel<Task>(10)

加上:

Kotlin

.buffer(10)

整体上可能变成:

Channel 10 个

Flow buffer 10 个

正在处理的 1 个

总共可能有 21 个左右处于系统中。

16. 总结

对于:

mChannel.receiveAsFlow()

.onEach {

}

.buffer(10)

bufferCapacity = 10 的意义是:

在 Flow 的 onEach 和后续下游之间创建一个容量为 10 的内部缓冲区,让上游最多可以领先下游 10 个元素。

实际开发中特别注意:

  1. buffer(10) 不是 mChannel 的容量;
  2. 它不是系统总容量限制;
  3. 它不是批处理大小;
  4. 默认缓冲区满了会挂起上游,不会丢数据;
  5. buffer 会让上下游并发执行;
  6. buffer 的位置非常重要;
  7. 放在 buffer 前面的 onEach 可能提前执行;
  8. 副作用、状态更新、消息确认等逻辑不要随便放在 buffer 前;
  9. 如果 mChannel 是无限容量,buffer(10) 不能防止内存积压;
  10. 多个 collector 收集 receiveAsFlow() 会竞争消费,不是广播;
  11. 如果需要广播,应考虑 SharedFlow;
  12. 如果需要整体容量控制,应优先控制 Channel 的容量,而不是只依赖 Flow 的 buffer。

一句话概括:

buffer(10) 控制的是 Flow 管道中某一段的预取和缓冲能力,它可以提升吞吐、解耦上下游,但不能替代 Channel 容量控制,也不能忽略副作用执行顺序。

相关推荐
恋猫de小郭1 小时前
一个 Linux 调度器优化,让 Android 多耗 20% 的电,传音工程师如何发现问题?
android·前端·ios
Kapaseker1 小时前
一个圆屏逼得我好好学习 Compose MeasurePolicy
android·kotlin
__Witheart__1 小时前
RK Android OTA U盘升级指南
android
__Witheart__1 小时前
RK Android OTA U盘升级包编译指南
android
我命由我123452 小时前
Android Service - Service 生命周期变化、Service 与 Activity 双向交互
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
不会Android的潘潘2 小时前
【AOSP 应用集成全方案】
android·aosp
编程猪猪侠2 小时前
基于uni-app-x 与 uni-app 的安卓与 H5 双向通信完整实现
android·javascript·uni-app
十六年开源服务商2 小时前
2026年WordPress建站新趋势与实战解决方案
android