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

一. SharedFlow 和 StateFlow 的介绍

在 Kotlin 中,SharedFlowStateFlow 都属于流(Flow)的扩展,专门用于处理状态和事件的流。它们在多种场景下都有广泛应用,但它们有本质的区别。

  • StateFlow :是一个特定的 SharedFlow,用于表示单一的状态,并且它始终保留并发发射的最新值。适用于需要记住最后一次值的场景,类似于 LiveData
  • SharedFlow:更为通用,适用于事件流的场景,它允许多次发射,但不会记住之前的值,默认不存储历史数据。

二. 冷流 vs. 热流

  • 冷流 (Cold Flow)是指只有在订阅时才会开始发射数据的流。Flow 就是冷流的典型代表,只有订阅者存在时,它才会开始执行。
  • 热流 (Hot Flow)则在数据发射后,无论是否有订阅者,都持续发射数据。SharedFlowStateFlow 都是热流的例子,特别适用于需要持续推送事件的场景。

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 仍然在发送数据,所以它只接收到 当时未被处理的事件 (可能是 34)。
  • 结论:没有活跃的订阅者时,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 必须等待订阅者处理完当前值

  • MutableSharedFlowextraBufferCapacity = 0replay = 0 的情况下,不会存储任何额外数据
  • 由于 collect消费速度比 emit 端慢delay(200) > delay(100)),emit 端会被频繁挂起,导致数据发射速率受限。

2. emit 变成一个同步阻塞点

  • sharedFlow.emit(i) 这里,emit 需要等待当前值被订阅者消费后,才能继续发射下一个值。
  • collectdelay(200),意味着每个值的消费至少需要 200ms ,而 emit 端每 100ms 产生一个新值。
  • 由于 extraBufferCapacity = 0emit 只能等待订阅者处理完当前值才能继续,因此 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:replayextraBufferCapacity 配合使用

当同时配置了 replayextraBufferCapacity,我们可以处理更复杂的缓存需求。此时,既有历史数据的重放,也有额外的缓存空间来缓存新事件。

示例:replayextraBufferCapacity 配合使用
  • 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 使得订阅者能够接收到最近的两个历史事件 12
  • 由于 extraBufferCapacity = 3,当订阅者消费速度慢(delay(300))时,多出来的数据会进入缓冲区,而不会立即阻塞 emit

小结

参数 作用
replay 存储最近 N 个值,新订阅者加入时可以立即收到
extraBufferCapacity 额外缓冲未消费的数据 ,防止 emit 被频繁挂起
replay + extraBufferCapacity 总缓存大小,决定 emit 是否挂起

适用场景

  • replay 适用于新订阅者需要过去数据的情况(如状态同步)。
  • extraBufferCapacity 适用于生产者比订阅者快 的情况,防止 emit 被挂起。
  • 两者结合 可以同时支持历史数据回放 + 提高吞吐量 ,让 emit 端尽可能不被阻塞。

场景 4:onBufferOverflow 策略

onBufferOverflow 策略解析

MutableSharedFlow 中,onBufferOverflow 参数用于控制当 emit 操作遇到缓冲区已满时的行为。它可以与 replayextraBufferCapacity 结合使用,以不同的方式处理数据溢出,确保数据流的稳定性和高效性。


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 提供了强大的数据广播能力,特别适用于多订阅者和多生产者的场景。通过合理配置 replayextraBufferCapacityonBufferOverflow,我们可以轻松实现历史数据的回放、缓存新数据以及灵活的溢出处理策略。

相关推荐
androidwork1 小时前
掌握 Kotlin Android 单元测试:MockK 框架深度实践指南
android·kotlin
田一一一1 小时前
Android framework 中间件开发(二)
android·中间件·framework
追随远方2 小时前
FFmpeg在Android开发中的核心价值是什么?
android·ffmpeg
神探阿航3 小时前
HNUST湖南科技大学-安卓Android期中复习
android·安卓·hnust
千里马-horse5 小时前
android vlc播放rtsp
android·media·rtsp·mediaplayer·vlc
難釋懷5 小时前
Android开发-文本输入
android·gitee
志存高远667 小时前
(面试)Android各版本新特性
android
IT从业者张某某7 小时前
信奥赛-刷题笔记-队列篇-T3-P3662Why Did the Cow Cross the Road II S
android·笔记
未来之窗软件服务7 小时前
Cacti 未经身份验证SQL注入漏洞
android·数据库·sql·服务器安全
BXCQ_xuan7 小时前
handsome主题美化及优化:10.1.0最新版 - 2
android