- 【Flow进阶篇一】SharedFlow 入门:冷流 vs. 热流的区别与基础用法 -【Flow进阶篇二】SharedFlow 缓存机制深度解析
- 【Flow进阶篇三】SharedFlow 与 StateFlow 的详细对比及适用场景
- SharedFlow 的订阅模式:单播 vs. 多播 vs. 重播(Replay)
- 详细分析 SharedFlow 处理多订阅者的策略
一. SharedFlow 和 StateFlow 的介绍
在 Kotlin 中,SharedFlow
和 StateFlow
都属于流(Flow)的扩展,专门用于处理状态和事件的流。它们在多种场景下都有广泛应用,但它们有本质的区别。
- StateFlow :是一个特定的
SharedFlow
,用于表示单一的状态,并且它始终保留并发发射的最新值。适用于需要记住最后一次值的场景,类似于LiveData
。 - SharedFlow:更为通用,适用于事件流的场景,它允许多次发射,但不会记住之前的值,默认不存储历史数据。
二. 冷流 vs. 热流
- 冷流 (Cold Flow)是指只有在订阅时才会开始发射数据的流。
Flow
就是冷流的典型代表,只有订阅者存在时,它才会开始执行。 - 热流 (Hot Flow)则在数据发射后,无论是否有订阅者,都持续发射数据。
SharedFlow
和StateFlow
都是热流的例子,特别适用于需要持续推送事件的场景。
SharedFlow
允许多个订阅者并行接收数据流,同时,它在发射事件时,不会被每个订阅者复制一份数据,这让它成为了处理广播型数据流的理想选择。
三. SharedFlow
的构造函数参数
MutableSharedFlow
构造函数包含三个参数,决定了如何缓存数据和处理溢出:
kotlin
public fun <T> MutableSharedFlow(
replay: Int = 0, // 重放次数
extraBufferCapacity: Int = 0, // 额外缓冲区容量
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND // 溢出策略
): MutableSharedFlow<T>
-
replay :表示历史数据的重放次数。指定该值后,订阅者可以接收到之前发射的数据。默认值为
0
,表示没有历史数据重放。 -
extraBufferCapacity :表示除了
replay
外,缓存的额外容量。默认值为0
,表示没有额外的缓存区。 -
onBufferOverflow:缓存区满时的溢出策略。可选的策略包括:
- BufferOverflow.SUSPEND(默认):缓存溢出时挂起。
- BufferOverflow.DROP_OLDEST:丢弃最旧的值。
- BufferOverflow.DROP_LATEST:丢弃最新的值。
四. 使用场景与示例
场景 1:只配置 replay
在这个场景中,我们将展示如何配置 replay
参数,允许订阅者接收到最近的几个历史数据。
先看下replay为0 情况
scss
fun main() {
runBlocking {
val sharedFlow = MutableSharedFlow<Int>(replay = 0)
// 发射一些事件
launch {
repeat(5) { i ->
sharedFlow.emit(i)
delay(100)
}
}
// 启动订阅者,接收回放的历史事件
launch {
delay(200) // 延迟启动
sharedFlow.collect { value ->
println("Subscriber received: $value")
}
}
delay(10*1000)
}
}
输出:
yaml
Subscriber received: 2
Subscriber received: 3
Subscriber received: 4
因为SharedFlow是热流,在没有订阅者的情况下还是会发射数据,所以前2次emit的数据就不会收到
接下来,只需要改动一行代码replay = 2
scss
fun main() {
runBlocking {
val sharedFlow = MutableSharedFlow<Int>(replay = 2)
// 发射一些事件
launch {
repeat(5) { i ->
sharedFlow.emit(i)
delay(100)
}
}
// 启动订阅者,接收回放的历史事件
launch {
delay(200) // 延迟启动
sharedFlow.collect { value ->
println("Subscriber received: $value")
}
}
delay(10*1000)
}
}
输出:
yaml
Subscriber received: 0 //收到缓存0
Subscriber received: 1 //收到缓存1
Subscriber received: 2 //正常依次接收2、3、4
Subscriber received: 3
Subscriber received: 4
场景 2:配置 extraBufferCapacity
在 SharedFlow
中,extraBufferCapacity
允许额外缓存一些数据。但是从网上文章看,很多人对这个参数没有理解,或者其它文章没有讲清楚?目前看上去的都是说replay + extraBufferCapacity
就是缓存容量的大小。那通过下面这个例子看看
scss
fun main() {
runBlocking {
val sharedFlow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 2)
// 发射一些事件
launch {
repeat(5) { i ->
sharedFlow.emit(i)
delay(100)
}
}
//启动订阅者,接收回放的历史事件
launch {
delay(200) // 延迟启动
sharedFlow.collect { value ->
println("Subscriber received: $value")
}
}
delay(10*1000)
}
}
输出:
yaml
Subscriber received: 2
Subscriber received: 3
Subscriber received: 4
分析:
replay = 0
,所以订阅者不会收到任何历史数据。extraBufferCapacity = 2
但没有订阅者时,前两个事件(0 和 1)并未被缓存。- 订阅者启动后,恰好
SharedFlow
仍然在发送数据,所以它只接收到 当时未被处理的事件 (可能是3
和4
)。 - 结论:没有活跃的订阅者时,
extraBufferCapacity
不能生效,事件直接丢失。
优化示例 :有活跃订阅者时 extraBufferCapacity
生效
scss
fun main() {
runBlocking {
val sharedFlow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 2)
//启动订阅者
launch {
sharedFlow.collect { value ->
println("Subscriber received: $value")
}
}
//确保已经开始订阅
delay(200)
// 发射一些事件
launch {
repeat(5) { i ->
sharedFlow.emit(i)
delay(100)
}
}
delay(10*1000)
}
}
输出:
yaml
Subscriber received: 0
Subscriber received: 1
Subscriber received: 2
Subscriber received: 3
Subscriber received: 4
结论: 有活跃的订阅者时,extraBufferCapacity
的缓存能生效。但是新的问题来了,这种用法和只配置replay = 2
结果是一致的,很明显这种用法也不是extraBufferCapacity
参数的设计初衷。
生产速度快于消费速度
extraBufferCapacity
设计的初衷就是为了解决这种场景的。也就是我们经常说的上下游数据处理速度不一致的情况,具体是下游(订阅者)处理比上游(发起者)慢。看看这个示例:
scss
fun main() {
runBlocking {
val sharedFlow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 0)
// 订阅者在事件发射前就已经启动
launch {
sharedFlow.collect { value ->
println("Subscriber received: $value")
// 模拟下游处理较慢的情况
delay(200)
}
}
//确保已经有订阅者
delay(100)
// 发射 5 个数据
launch {
repeat(5) { i ->
println("Emitted: $i")
sharedFlow.emit(i)
delay(100)
}
}
delay(10 * 1000)
}
}
问题分析
1. extraBufferCapacity = 0
,导致 emit
必须等待订阅者处理完当前值
MutableSharedFlow
在extraBufferCapacity = 0
且replay = 0
的情况下,不会存储任何额外数据。- 由于
collect
端消费速度比emit
端慢 (delay(200)
>delay(100)
),emit
端会被频繁挂起,导致数据发射速率受限。
2. emit
变成一个同步阻塞点
- 在
sharedFlow.emit(i)
这里,emit
需要等待当前值被订阅者消费后,才能继续发射下一个值。 - 但
collect
端delay(200)
,意味着每个值的消费至少需要 200ms ,而emit
端每100ms
产生一个新值。 - 由于
extraBufferCapacity = 0
,emit
只能等待订阅者处理完当前值才能继续,因此emit
实际上会被卡住,导致每 200ms 才能发射一个值,而不是 100ms 一次。
终极方案
scss
fun main() {
runBlocking {
val sharedFlow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 3)
// 订阅者在事件发射前就已经启动
launch {
sharedFlow.collect { value ->
println("Subscriber received: $value")
// 模拟下游处理较慢的情况
delay(300)
}
}
//确保已经有订阅者
delay(100)
// 发射 5 个数据
launch {
repeat(5) { i ->
println("Emitted: $i")
sharedFlow.emit(i)
delay(100)
}
}
delay(10 * 1000)
}
}
分析
extraBufferCapacity = 3
允许emit
存入最多 3 个未被消费的数据,而不会立即被挂起。- 这样
上游
不必严格等待下游
处理完当前数据,可以更快地发射数据,提升吞吐量。
场景 3:replay
与 extraBufferCapacity
配合使用
当同时配置了 replay
和 extraBufferCapacity
,我们可以处理更复杂的缓存需求。此时,既有历史数据的重放,也有额外的缓存空间来缓存新事件。
示例:replay
与 extraBufferCapacity
配合使用
-
replay
(回放历史数据)- 记录最近
N
个数据,新订阅者加入时会立即收到这些数据。 - 适用于状态同步 、最近消息回放等场景。
- 记录最近
-
extraBufferCapacity
(额外缓冲区)- 允许
emit
在订阅者消费速度较慢时,额外存储N
个数据,防止emit
挂起。 - 适用于数据流速较快 ,但订阅者处理较慢的情况,如 WebSocket 消息流、传感器数据等。
- 允许
-
两者结合
replay
确保新订阅者不丢失关键数据。extraBufferCapacity
允许emit
继续推送新数据,避免因订阅者处理慢而阻塞。
scss
fun main() {
runBlocking {
// replay = 2(存储最近 2 个值),extraBufferCapacity = 3(额外缓冲 3 个)
val sharedFlow = MutableSharedFlow<Int>(replay = 2, extraBufferCapacity = 3)
// 先发射 3 个数据(只有最近的 2 个会被 replay 记录)
repeat(3) { i ->
println("Emitting: $i")
sharedFlow.emit(i)
}
// 确保 replay 数据已发送完毕
delay(100)
// 启动订阅者(应该立即收到 replay 最新的2个数据)
launch {
sharedFlow.collect { value ->
println("Subscriber received: $value")
// 模拟下游处理数据较慢
delay(300)
}
}
// 确保订阅者已启动
delay(500)
// 再次发射 5 个数据
launch {
repeat(5) { i ->
println("Emitting: ${i + 3}")
sharedFlow.emit(i + 3)
delay(100)
}
}
delay(10 * 1000)
}
}
输出:
yaml
Emitting: 0
Emitting: 1
Emitting: 2
Subscriber received: 1 <-- replay 记录的第一个数据
Subscriber received: 2 <-- replay 记录的第二个数据
Emitting: 3
Emitting: 4
Subscriber received: 3
Emitting: 5
Emitting: 6
Emitting: 7 <-- extraBufferCapacity 缓冲新数据,防止 emit 挂起
Subscriber received: 4
Subscriber received: 5
Subscriber received: 6
Subscriber received: 7
分析:
replay = 2
使得订阅者能够接收到最近的两个历史事件1
和2
。- 由于
extraBufferCapacity = 3
,当订阅者消费速度慢(delay(300)
)时,多出来的数据会进入缓冲区,而不会立即阻塞emit
。
小结
参数 | 作用 |
---|---|
replay |
存储最近 N 个值,新订阅者加入时可以立即收到 |
extraBufferCapacity |
额外缓冲未消费的数据 ,防止 emit 被频繁挂起 |
replay + extraBufferCapacity |
总缓存大小,决定 emit 是否挂起 |
适用场景
replay
适用于新订阅者需要过去数据的情况(如状态同步)。extraBufferCapacity
适用于生产者比订阅者快 的情况,防止emit
被挂起。- 两者结合 可以同时支持历史数据回放 + 提高吞吐量 ,让
emit
端尽可能不被阻塞。
场景 4:onBufferOverflow
策略
onBufferOverflow 策略解析
在 MutableSharedFlow
中,onBufferOverflow
参数用于控制当 emit
操作遇到缓冲区已满时的行为。它可以与 replay
和 extraBufferCapacity
结合使用,以不同的方式处理数据溢出,确保数据流的稳定性和高效性。
1. onBufferOverflow 的三种策略
当 emit
发送数据时,如果 MutableSharedFlow
的缓冲区已满,onBufferOverflow
决定数据该如何处理,主要有以下三种策略:
策略 | 行为 | 适用场景 |
---|---|---|
SUSPEND(默认) | 发送数据时,如果缓冲区已满,emit 会被挂起,直到有空间可用 |
适用于数据不可丢失,必须严格按序处理的情况,如数据库日志写入 |
DROP_OLDEST | 丢弃最早的(旧的)数据,腾出空间给新数据 | 适用于只关心最新数据的场景,如股票行情、传感器数据 |
DROP_LATEST | 丢弃最新的数据,即当前 emit 的数据直接丢弃,不会进入缓冲区 |
适用于必须处理所有数据但可以牺牲部分新数据的情况,如限流保护 |
2. 示例:onBufferOverflow 三种策略
以下示例演示 onBufferOverflow
的不同策略如何影响 MutableSharedFlow
的行为。
scss
fun main() {
runBlocking {
val sharedFlow = MutableSharedFlow<Int>(
replay = 0,
extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.DROP_OLDEST // 选择策略
)
// 启动订阅者,模拟慢速处理
launch {
sharedFlow.collect { value ->
println("Subscriber received: $value")
delay(300) // 订阅者处理较慢
}
}
// 确保订阅者已启动
delay(100)
// 发送 5 个数据(超出缓冲区大小)
launch {
repeat(5) { i ->
println("Emitting: $i")
sharedFlow.emit(i)
delay(100)
}
}
delay(3000) // 运行足够时间以观察效果
}
}
🔹 预期输出(使用 DROP_OLDEST
)
yaml
Emitting: 0
Emitting: 1
Subscriber received: 0
Emitting: 2
Emitting: 3
Emitting: 4
Subscriber received: 3 <-- 旧数据 1,2 被丢弃,3 被保留
Subscriber received: 4
分析:
- 由于
DROP_OLDEST
,当缓冲区满时,旧数据(如1, 2
)被丢弃,确保新的数据(如3, 4
)进入缓冲区并被消费。 - 如果策略是
DROP_LATEST
,则会丢弃最新的数据4
。 - 如果策略是
SUSPEND
,则上游发射端会在缓冲区满了的时候挂起。
3. 适用场景
策略 | 适用场景 |
---|---|
SUSPEND | 日志系统 (确保数据顺序完整)、事件总线(保证所有事件都能被处理) |
DROP_OLDEST | 股票交易、实时传感器数据 (只关注最新数据)、视频流(丢弃旧帧保证实时性) |
DROP_LATEST | API 限流 (避免过载)、消息队列(防止高峰期压力过大) |
4. 结论
SUSPEND
适用于数据不可丢失 的情况,但可能导致emit
挂起。DROP_OLDEST
适用于实时性强的应用,如股票数据、传感器监测。DROP_LATEST
适用于高吞吐量但可丢数据的场景,如 API 限流或队列控制。
5. 总结
SharedFlow
提供了强大的数据广播能力,特别适用于多订阅者和多生产者的场景。通过合理配置 replay
、extraBufferCapacity
和 onBufferOverflow
,我们可以轻松实现历史数据的回放、缓存新数据以及灵活的溢出处理策略。