【Flow进阶篇一】SharedFlow 入门:冷流 vs. 热流的区别与基础用法

一. 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,我们可以轻松实现历史数据的回放、缓存新数据以及灵活的溢出处理策略。

相关推荐
传奇开心果编程14 分钟前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓2 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1233 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium
Dovis(誓平步青云)3 小时前
导览音频切换太快,旧讲解不能覆盖新展品
android·前端·javascript·ecmascript·音视频·宠物
熊猫钓鱼>_>19 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 Reaktive 实现响应式原语适配(完整版 · 含摘要目录与技术图表)
华为·kotlin·大模型·ai编程·harmonyos·适配·reaktive
五彩小白20 小时前
自回归和上下文
android