
前言
化神初成,你已点燃热流之火。StateFlow 的状态管理让你告别了屏幕旋转的重复请求,SharedFlow 的事件分发让导航与 Toast 井井有条。你觉得自己已经掌握了热流的全部。
然而,生产环境的复杂需求很快会让你陷入新的困惑:
"我的
SharedFlow偶尔会丢失事件,是不是缓冲区设小了?" "新打开的页面有时收不到之前发射的'加载完成'事件,该设replay吗?" "项目里老代码还在用Channel做事件总线,我该不该重构为SharedFlow?两者到底有什么区别?" "replayCache是什么?它和replay参数是一回事吗?"
这些问题的答案,藏在 SharedFlow 的底层设计中。它并非一个简单的"多播版 Channel",而是一套精密的事件分发系统------有独立的缓存策略、缓冲区管理和背压处理逻辑。理解这些配置项,你才能真正驾驭事件流。
本讲是化神境的中阶修炼。你将:
- 彻底搞懂
replay与extraBufferCapacity的区别与协作机制。 - 看懂
replayCache的内部结构,明白新订阅者如何收到历史事件。 - 掌握
BufferOverflow策略在SharedFlow中的实际效果。 - 理清
SharedFlow与Channel的本质差异,知道何时该用哪个。 - 学会用
shareIn将冷流转换为可配置的SharedFlow。
准备好揭开 SharedFlow 的底层面纱了吗?我们开始。
操千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意。
SharedFlow 配置全景图
在前一讲中,我们使用了最简单的 MutableSharedFlow()。现在,让我们直面它的完整构造函数:
kotlin
public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>
这三个参数共同决定了 SharedFlow 的行为。它们不是孤立的开关,而是一个三维调节旋钮------调节着事件的缓存、缓冲和丢弃策略。
replay:新订阅者加入时,能立即收到的历史事件数量 。这些事件存储在replayCache中。extraBufferCapacity:除了replayCache之外,为慢速收集者准备的额外缓冲区大小。onBufferOverflow:当缓冲区(replayCache+ 额外缓冲区)已满时,如何处理新发射的事件。
总缓冲区容量 = replay + extraBufferCapacity。
replay:新订阅者的"时光机"
replay 是 SharedFlow 最具特色的能力。它像一个环形缓冲区 ,始终保留最近 replay 个已发射的值。当新订阅者调用 collect 时,SharedFlow 会先将 replayCache 中的所有值依次发送给它,然后才开始发送后续的新值。
kotlin
val flow = MutableSharedFlow<Int>(replay = 2)
runBlocking {
flow.emit(1)
flow.emit(2)
flow.emit(3) // 此时 replayCache 为 [2, 3]
flow.collect { println("订阅者1: $it") } // 立即打印 2, 3,然后等待新值
}
replay 的典型场景
replay 值 |
适用场景 | 示例 |
|---|---|---|
0(默认) |
纯事件,新订阅者无需知道历史 | Toast 提示、埋点上报 |
1 |
需要知道"最近一次"状态 | 网络连接状态、当前播放歌曲 |
>1 |
需要回溯近期历史 | 聊天室最后 N 条消息、日志流 |
注意 :replay 会占用内存。如果缓存的是大对象(如 Bitmap),需谨慎设置。
extraBufferCapacity 与 BufferOverflow:慢速收集者的缓冲池
当收集者的处理速度跟不上发射速度时,extraBufferCapacity 提供了缓冲空间。
无额外缓冲时的行为
kotlin
val flow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 0)
// 慢速收集者
launch {
flow.collect {
delay(1000)
println("收到: $it")
}
}
// 快速发射
repeat(5) {
flow.emit(it) // 第 2 次 emit 就会挂起,等待收集者处理完第 1 个
}
当 extraBufferCapacity = 0 时,发射者的 emit 是挂起的,必须等待所有收集者处理完当前值才能继续。这保证了"每个值都被所有收集者处理",但牺牲了发射速度。
引入额外缓冲
kotlin
val flow = MutableSharedFlow<Int>(
replay = 0,
extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.SUSPEND // 默认
)
现在,发射者可以连续 emit 2 个值而不会挂起(存入额外缓冲区),第 3 个 emit 时才会挂起等待。
BufferOverflow 策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
SUSPEND(默认) |
缓冲区满时,emit 挂起,等待空间 |
必须保证不丢失任何事件 |
DROP_OLDEST |
丢弃缓冲区中最旧的值,为新值腾空间 | 只关心最新状态,如位置更新 |
DROP_LATEST |
丢弃最新发射的值,保留旧值 | 极少用,保护历史数据 |
实战:用 DROP_OLDEST 实现位置更新
kotlin
class LocationViewModel : ViewModel() {
private val _locations = MutableSharedFlow<Location>(
replay = 1, // 新页面立即收到最新位置
extraBufferCapacity = 2, // 允许积压 2 个
onBufferOverflow = BufferOverflow.DROP_OLDEST // 积压满时丢弃最旧
)
val locations: SharedFlow<Location> = _locations.asSharedFlow()
fun onLocationUpdate(location: Location) {
viewModelScope.launch {
_locations.emit(location) // 永远不会挂起,旧位置自动丢弃
}
}
}
SharedFlow vs Channel:一对多通信的抉择
在协程早期,Channel 常被用作事件总线。SharedFlow 出现后,两者如何选择?
| 对比维度 | SharedFlow | Channel |
|---|---|---|
| 设计目的 | 多播事件流,多个订阅者 | 协程间通信,通常一对一 |
| 消费模型 | 每个订阅者都收到所有值(受 replay/缓冲影响) | 每个值只能被一个消费者接收 |
| 历史重放 | 支持 replay |
不支持 |
| 缓冲区 | 可配置,支持丢弃策略 | 可配置,但无 replay |
| 关闭 | 无需关闭 | 需要 close(),否则可能泄漏 |
| 典型场景 | UI 事件、状态广播 | 生产者-消费者队列、Actor 模式 |
结论:
- 需要多个组件同时响应同一事件 (如导航、全局消息)→
SharedFlow。 - 需要任务队列 、生产者-消费者 模式 →
Channel。
shareIn:将冷流转换为可配置的 SharedFlow
前一讲我们轻触了 shareIn,本讲深入它的配置。shareIn 的完整签名:
kotlin
fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T>
注意:shareIn 产生的 SharedFlow 不支持 extraBufferCapacity 和 onBufferOverflow 配置。它的缓冲区策略由上游 Flow 的背压行为决定。如果需要精细控制缓冲区,应先使用 buffer 操作符。
kotlin
val sharedFlow = coldFlow
.buffer(capacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST)
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
replay = 1
)
实战:用 SharedFlow 构建全局消息总线
kotlin
object GlobalMessageBus {
private val _messages = MutableSharedFlow<Message>(
replay = 0, // 新订阅者不收到历史
extraBufferCapacity = 64, // 允许积压 64 条
onBufferOverflow = BufferOverflow.DROP_OLDEST // 满时丢弃最旧
)
val messages: SharedFlow<Message> = _messages.asSharedFlow()
suspend fun post(message: Message) {
_messages.emit(message)
}
data class Message(val content: String, val type: MessageType)
enum class MessageType { TOAST, SNACKBAR, DIALOG }
}
// 在 Activity 或 ViewModel 中收集
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
GlobalMessageBus.messages.collect { message ->
when (message.type) {
MessageType.TOAST -> Toast.makeText(this@MainActivity, message.content, Toast.LENGTH_SHORT).show()
// 处理其他类型...
}
}
}
}
}
常见错误与避坑指南
错误 1:误以为 replay 会持久化
kotlin
// ❌ 错误:replay 只在内存中缓存,进程被杀后丢失
val flow = MutableSharedFlow<Int>(replay = 1)
// 应用被杀重启后,新订阅者收不到上次的值
正确理解 :replayCache 是内存缓存,不持久化。如需持久化,配合 DataStore 或 Room。
错误 2:将 SharedFlow 用于单播队列
kotlin
// ❌ 滥用:用 SharedFlow 做任务队列
val taskQueue = MutableSharedFlow<Task>()
// 多个工人抢任务,无法保证每个任务只被处理一次
正确做法 :任务队列用 Channel。
错误 3:忘记处理缓冲区溢出
kotlin
val flow = MutableSharedFlow<Int>(extraBufferCapacity = 0)
// 发射过快时,emit 会挂起,可能导致上游协程阻塞
优化 :根据业务需求选择合适的 onBufferOverflow。
错误 4:在 shareIn 后继续使用 buffer 期望生效
kotlin
coldFlow.shareIn(...).buffer(...) // buffer 作用在热流上,效果有限
正确顺序 :coldFlow.buffer(...).shareIn(...)。
八、最佳实践
- 区分事件与状态 :状态用
StateFlow,事件用SharedFlow。 - 合理设置 replay :需要新订阅者感知历史时才设
>0,避免内存浪费。 - 配置缓冲区策略 :根据是否允许丢事件选择
SUSPEND或DROP_OLDEST。 - 全局消息总线用
replay=0+DROP_OLDEST:防止内存膨胀。 - 用
shareIn时注意操作符顺序 :buffer、onEach等应在shareIn之前。 - 在 ViewModel 中使用
WhileSubscribed:避免后台无意义的上游执行。
九、总结与下回预告
恭喜,你已掌握 SharedFlow 的高级配置,化神境中阶修炼完成!
本讲核心收获:
replay决定新订阅者收到的历史事件数,存储在replayCache中。extraBufferCapacity+onBufferOverflow控制慢速收集者的缓冲与丢弃策略。SharedFlow用于多播事件,Channel用于单播队列,两者各司其职。shareIn将冷流转换为 SharedFlow,注意操作符顺序。
在下一讲 【化神境 · 后阶】 中,我们将深入 Flow 的异常处理机制:catch 与 retry 的进阶用法、如何处理 collect 块的异常、以及 Flow 与结构化并发的协同。届时你会明白:
catch真的能捕获所有异常吗?retryWhen如何实现指数退避重试?- 如何设计"永不失败"的 UI 状态流?
【当前境界修为面板】
| 当前境界 | 修炼技能 | 修炼进度 | 修炼心得 |
|---|---|---|---|
| 化神境 · 中阶 | 1、replayCache 时光回溯术 2、BufferOverflow 取舍之道 3、shareIn 完全配置诀 |
当前进度 :65% 修为 :650/1000 下一突破 :[化神境 · 后阶] (需领悟:catch/retry 进阶、异常透明性、Flow 与结构化并发) |
replay是新订阅者的时光机,DROP_OLDEST是只关心最新的取舍之道。 |
【本讲思考题】
-
表象题:以下 SharedFlow 的总缓冲区容量是多少?
kotlinMutableSharedFlow<Int>(replay = 2, extraBufferCapacity = 3) -
场景题:你需要在 ViewModel 中监听一个高频更新的传感器 Flow,并广播给多个 UI 组件。你希望新订阅的组件能立即收到最近 1 个传感器值,且当 UI 处理不过来时丢弃旧值。请写出 SharedFlow 的配置。
-
原理题 :
SharedFlow的replayCache是如何实现"新订阅者先收到历史值"的?collect函数内部是如何处理这个逻辑的?请结合源码简述。
道友,化神境的最后一道关隘已在眼前。掌握了异常处理,你的 Flow 管道将坚不可摧。化神境·后阶见。
欢迎一键四连 (
关注+点赞+收藏+评论)