4. MutableSharedFlow 创建与基本使用:replay、extraBufferCapacity、onBufferOverflow 参数

MutableSharedFlow 是 Kotlin 协程中用于广播事件的多发射器-多收集器热流。与 StateFlow 不同,它不维护状态快照,而是通过缓冲区管理历史事件。本章深入剖析其构造函数中的三个核心参数——replay、extraBufferCapacity、onBufferOverflow,并给出典型实现流程和调试方法。

概念辨析: SharedFlow 设计目标为事件流(click、toast、导航命令),不保证最新值,但能通过缓冲区重放历史事件;StateFlow 则保证始终拥有最新值且只保留一个值。

4.1 构造函数与参数语义

MutableSharedFlow 的创建接口为:

fun  MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow
参数 默认值 作用 典型场景
replay 0 新收集器订阅时,从缓冲区重放最近的 n 个事件 保留最后一条消息(replay=1)或保留多条(如通知列表)
extraBufferCapacity 0 在 replay 缓冲区之外额外开辟的缓存槽位,用于暂存来不及处理的事件 高吞吐事件、收集器短暂滞后时不阻塞发射
onBufferOverflow SUSPEND 当缓冲区填满时的策略:SUSPEND(挂起发射)、DROP_OLDEST(丢弃最旧)、DROP_LATEST(丢弃最新) 抗背压:丢旧或丢新取决于业务容忍度

4.2 缓冲区结构原理

SharedFlow 底层维护一个环形缓冲区,由两部分串联构成:重放区(replay) + 额外缓存区(extraBufferCapacity)。总可用缓冲区大小 = replay + extraBufferCapacity。当收集器处理速度慢于发射速度时,事件积压至缓冲区;若缓冲区满,则触发 onBufferOverflow 策略。

关键等式: 缓冲区总容量 = replay + extraBufferCapacity。实际可暂存的事件数不超过此值;若 replay 为 0 且 extraBufferCapacity 为 0,则除非有挂起收集器,否则新事件会立即触发 SUSPEND。
常见错误: 将 replay 设为 0 且 extraBufferCapacity 设为 0,同时 onBufferOverflow 设为 DROP_OLDEST,会使得新事件在无收集器时直接被丢弃,造成事件丢失。
MutableSharedFlow 缓冲区与溢出处理流程 重放区 (replay) 留存历史事件,供新收集器重放 额外缓存区 (extraBufferCapacity) 暂存溢出事件,防止发射挂起 发射器 emit 环形缓冲区(replay + extraBufferCapacity) 缓冲区已满? SUSPEND(默认) DROP_OLDEST 或 DROP_LATEST 📌 溢出策略选择 • SUSPEND:发射协程挂起等待 • DROP_OLDEST:丢弃最旧事件 • DROP_LATEST:丢弃当前事件

4.3 实现流程与代码示例

下面用三个典型配置演示不同参数组合的行为。假定收集器启动有 2 秒延迟,发射器持续发射 5 条消息。

4.3.1 仅重放(replay=2, extraBufferCapacity=0, SUSPEND)

val sharedFlow = MutableSharedFlow<String>(replay = 2)
scope.launch {
    delay(2000)  // 模拟收集器延迟
    sharedFlow.collect { msg ->
        println("收集到: $msg")
    }
}
sharedFlow.tryEmit("A")
sharedFlow.tryEmit("B")
sharedFlow.tryEmit("C")
  • 预期输出: 收集器启动后先收到 "B"、"C"(重放最近 2 条),然后不再收到 "A"。符合 replayed 语义。
  • 注意: extraBufferCapacity=0 且 onBufferOverflow=SUSPEND 时,若收集器未启动,tryEmit 会立即失败(返回 false)——这里使用 tryEmit 意在展示失败,实际应使用 emit 挂起。

4.3.2 额外缓冲 + 溢出丢弃(replay=0, extraBufferCapacity=2, DROP_OLDEST)

val sharedFlow = MutableSharedFlow<String>(
    replay = 0,
    extraBufferCapacity = 2,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)
scope.launch {
    delay(2000)
    sharedFlow.collect { msg ->
        println("收集到: $msg")
    }
}
sharedFlow.emit("1")
sharedFlow.emit("2")
sharedFlow.emit("3")
sharedFlow.emit("4")
// 此时额外缓冲已满(1,2,3 留存?实际因 DROP_OLDEST 会丢弃最旧的)
// 收集器启动后收到什么?
  • 缓冲演变:发射 "1" → 入队;"2" → [1,2];"3" → 满,丢弃最旧 "1",变为 [2,3];"4" → 丢弃 "2",变为 [3,4]。
  • 收集器启动后重放 0(replay=0),无历史重放,但是额外缓冲区中仍存有 [3,4] 会立即发给新收集器吗?注意: 重放区为 0,额外缓冲区中的事件不会自动发给新收集器,它们只用于防背压;新收集器只能收到之后发射的事件。此处易误解,需谨慎。
易错点: 只有 replay 区的内容才会在新收集器订阅时重放;extraBufferCapacity 仅用于缓冲发射端,不会传递给新订阅者。要同时支持回放和防压,请设置合适的 replay 值。

4.3.3 全缓冲 + 挂起(replay=1, extraBufferCapacity=1, SUSPEND)

val sharedFlow = MutableSharedFlow<String>(
    replay = 1,
    extraBufferCapacity = 1,
    onBufferOverflow = BufferOverflow.SUSPEND
)
scope.launch {
    delay(2000)
    sharedFlow.collect { msg ->
        delay(500)  // 慢收集
        println("处理: $msg")
    }
}
scope.launch {
    sharedFlow.emit("X")
    sharedFlow.emit("Y")
    sharedFlow.emit("Z")  // 当缓冲满(replay=1 + extra=1 = 2),第三个 emit 会挂起
}
  • 总缓冲容量 = 2;积累 X、Y 后,Z 发射时缓冲满,发射挂起直到有收集器消费移出事件。
  • 收集器启动后先收到重放事件 X(replay=1),然后消费 Y、Z,顺序正确。

4.4 调试与性能优化

场景 调试手段 建议
事件丢失 检查 replay 与 extraBufferCapacity 是否为零,配合 DROP 策略 使用 .subscriptionCount 观察收集器数量
发射挂起导致死锁 添加超时 withTimeout 包裹 emit,打印堆栈 确保收集器及时取消或使用 J.U.C 测试工具
内存泄漏 监听 SharedFlow 的 subscriptionCount 为 0 时取消 若 replay 过大且长期无收集器,内存占用上升
背压优化 增大 extraBufferCapacity 并使用 DROP_OLDEST 适用于高频率传感器读数,丢掉旧值保留新值

4.5 常见错误与避坑

  1. 误以为 replay 包含额外缓冲区: 新收集器只能得到 replay 区中的事件,额外缓冲区中的事件仅用于暂存来不及下发的待消费事件,消费后即删除。
  2. 忘记设置 onBufferOverflow 为 DROP 可能导致无限挂起: 默认 SUSPEND,若收集器永远不消费,发射协程永久挂起。
  3. 过度依赖 tryEmit: tryEmit 返回 Boolean 表示是否成功送入缓冲区;若缓冲满且策略为 SUSPEND,tryEmit 会直接返回 false 并丢弃事件,容易造成静默丢失。
  4. 混淆 MutableSharedFlow 与 MutableStateFlow: 前者没有初始值,后者必须给初始值且 replay 固定为 1。
最佳实践: 对于事件流(如 UI 导航、一次性提醒),推荐 MutableSharedFlow<T>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST),既保证不阻塞发射,又不会重放过期事件。

4.6 稳定性建议

  • 在 ViewModel 中创建 SharedFlow 后,使用 viewModelScope 管理协程生命周期。
  • 若需在多个 Collect 之间共享缓冲状态,考虑将 SharedFlow 设为单例或注入 Dagger / Hilt 单例。
  • 对高吞吐流(每秒千次以上)采用 extraBufferCapacity 不小于 64,并配合 DROP_OLDEST 避免 OOM。
  • 使用 .replayCache 属性检查当前重放缓存内容,辅助调试。