Kotlin 学习笔记(六)—— Flow 数据流学习实践指北(二)StateFlow 与 SharedFlow(下)

简介: Kotlin 学习笔记(六)—— Flow 数据流学习实践指北(二)StateFlow 与 SharedFlow(下)

打印出的时间戳几乎是一样的,若严格按照 log 打印的时间戳顺序,应该是 Emitter 先发送的 4,Subscriber1 再才接收到的 2,但根据反复实践的结果来看,实际上是 Subscriber1 先接收缓冲区中的 2,等缓冲区有剩余空间后,Emitter 才结束挂起继续发送 4. 把上面的例子简化一下,再改改数据:

//code 7
private fun coroutineStudy() {
    val sharedFlow = MutableSharedFlow<Int>(replay = 1, extraBufferCapacity = 1)
    lifecycleScope.launch {
        launch {
            sharedFlow.collect {
                println("++++ sharedFlow1 collected $it")
                delay(10000)
            }
        }
        launch {
            (1..4).forEach{
                sharedFlow.emit(it)
                println("+++emit $it")
                delay(1000)
            }
        }
    }
}

打印结果如下所示,因为把 sharedFlow delay 的时长设置为 10s,所以很明显地看到 Emitter 在发送 1、2、3 时时间间隔均是 1s,发送 4 时足足过了 8s,这段时间就是 Emitter 被挂起了,一直等到 sharedFlow1 接收到 2 之后,4 才被 Emitter 发送,而 sharedFlow1 的每次接收都是间隔 10s,所以是先接收的 2,再结束挂起发送的 4.

00:25:52.481 29483-29483/com.example.myapplication I/System.out: +++emit 1
00:25:52.482 29483-29483/com.example.myapplication I/System.out: ++++ sharedFlow1 collected 1
00:25:53.483 29483-29483/com.example.myapplication I/System.out: +++emit 2
00:25:54.486 29483-29483/com.example.myapplication I/System.out: +++emit 3
00:26:02.487 29483-29483/com.example.myapplication I/System.out: +++emit 4
00:26:02.488 29483-29483/com.example.myapplication I/System.out: ++++ sharedFlow1 collected 2
00:26:12.497 29483-29483/com.example.myapplication I/System.out: ++++ sharedFlow1 collected 3
00:26:22.516 29483-29483/com.example.myapplication I/System.out: ++++ sharedFlow1 collected 4

通过源码也可看出这个结论,从 collect 方法进入,最终可以找到实际上是调用了 SharedFlowImpl 中的 collect 方法:

//code 8
    override suspend fun collect(collector: FlowCollector<T>) {
        val slot = allocateSlot()
        try {
            if (collector is SubscribedFlowCollector) collector.onSubscription()
            val collectorJob = currentCoroutineContext()[Job]
            while (true) {
                var newValue: Any?
                while (true) {
                    newValue = tryTakeValue(slot) //首先尝试直接获取值
                    if (newValue !== NO_VALUE) break
                    awaitValue(slot) //没获取到则只能挂起等待新值到来
                }
                collectorJob?.ensureActive()
                collector.emit(newValue as T)
            }
        } finally {
            freeSlot(slot)
        }
    }

在内层 while 循环中,首先是通过 tryTakeValue 方法直接取值,如果没取到则通过 awaitValue 方法挂起等待新值,awaitValue 是个挂起函数。取到新值之后,才会跳出内层 while 循环,并执行 collector.emit(newValue as T),而这一段代码,实际上就是调用的 code 7 中的 sharedFlow.emit(it) 代码。

此处源代码还可以看出,SharedFlow 每次在 emit 之前,确实都会查看所在协程是否还在运行;且它确实是不会停止的,哪怕没有接收到新值,也会一直处于挂起等待的状态,想要结束则得使用截断类型的操作符。


1.3 onBufferOverflow


SharedFlow 构造函数的第三个参数就是设置超过 Buffer 之后的策略,默认是将生产者挂起暂时不再发送数据,即 BufferOverflow.SUSPEND。

还有另外两个数据丢弃策略:

1)BufferOverflow.DROP_LATEST 丢弃最新数据;

image.png

Emitter 在发送 4 时,因为 Buffer 已满,所以只能按照策略将最新的数据 4 丢弃。而在发送 3 时,由于 1 已经被消费过,所以可以从 Buffer 中移除,从而腾出存储空间缓存 3。

2)BufferOverflow.DROP_OLDEST 丢弃最老数据:

image.png

这个策略就比较简单,Buffer 中只会存储最新的数据。不管较老的数据是否被消费,当 Buffer 已满而又有新的数据到达时,老数据都会从 Buffer 中移除,腾出空间让给新数据。

注意点:当 replay、extra 都为 0,即没有 Buffer 的时候,那么 onBufferOverflow 只能是 BufferOverflow.SUSPEND。丢弃策略启动的前提是 SharedFlow 至少有 Buffer 且 Buffer 已满。


1.4 emit 与 tryEmit


由前一节可知,当 SharedFlow 的 Buffer 已满且 onBufferOverflow 为 BufferOverflow.SUSPEND 的时候,emit 会被挂起(emit 是个挂起函数),但这会影响到 Emitter 的速度。如果不想在发送数据的时候被挂起,除了设置 onBufferOverflow 丢弃策略外,还可以使用 tryEmit 方法。

//code 9
    override fun tryEmit(value: T): Boolean {
        var resumes: Array<Continuation<Unit>?> = EMPTY_RESUMES
        val emitted = synchronized(this) {
            if (tryEmitLocked(value)) {
                resumes = findSlotsToResumeLocked(resumes)
                true
            } else {
                false
            }
        }
        for (cont in resumes) cont?.resume(Unit)
        return emitted
    }
    @Suppress("UNCHECKED_CAST")
    private fun tryEmitLocked(value: T): Boolean {
        // Fast path without collectors -> no buffering
        // 1.没有订阅者时,直接返回 true,因为没有人接收,发了也没用,也不用缓存
        if (nCollectors == 0) return tryEmitNoCollectorsLocked(value) // always returns true
        // With collectors we'll have to buffer
        // 2.有订阅者,就得考虑缓存发送的值了
        // cannot emit now if buffer is full & blocked by slow collectors
        // 3.如果缓存空间已满,且订阅者还在挂起处理上次的数据,则不能 emit
        if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) {
            when (onBufferOverflow) {
                BufferOverflow.SUSPEND -> return false // will suspend
                BufferOverflow.DROP_LATEST -> return true // just drop incoming
                BufferOverflow.DROP_OLDEST -> {} // force enqueue & drop oldest instead
            }
        }
        // 4.代码能走到这里,说明缓存还有空间或丢弃策略为DROP_OLDEST
        enqueueLocked(value)
        bufferSize++ // value was added to buffer
        // drop oldest from the buffer if it became more than bufferCapacity
        if (bufferSize > bufferCapacity) dropOldestLocked()
        // keep replaySize not larger that needed
        if (replaySize > replay) { // increment replayIndex by one
            updateBufferLocked(replayIndex + 1, minCollectorIndex, bufferEndIndex, queueEndIndex)
        }
        return true
    }

由代码可见 tryEmit 不是一个挂起函数,它有返回值,如果返回 true 则说明发送数据成功了;如果返回 false,则说明这时发送数据需要被挂起等待。其中最主要的就是 tryEmitLocked 方法。

tryEmitLocked 方法主要逻辑已在注释中说明,需要额外说明的是,bufferCapacity 就是 replay + extraBufferCapacity 的大小;replayIndex 指的是最近开始订阅的订阅者在 replay cache 缓存数组中需要重播的最小 index。所以当使用默认构造的 SharedFlow 时,replayextraBufferCapacity 都为 0,如果这时再使用 tryEmit 方法进行发送,则会使得 if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) 判断为 true,默认的丢弃策略又是 BufferOverflow.SUSPEND,就会导致这里会直接返回 false,永远都不会发送出值。所以,在使用默认构造的 SharedFlow 时,不能使用 tryEmit 发送值,否则无法发送。 一般使用 emit 即可。

在 SharedFlow 具体实现中,emit 方法就是先尝试使用 tryEmit 来发送值,如果不能马上发送再使用挂起函数 emitSuspend 方法:

//code 10    class SharedFlowImpl
    override suspend fun emit(value: T) {
        if (tryEmit(value)) return // fast-path
        emitSuspend(value)
    }


2. StateFlow


看完 SharedFlow 再来看 StateFlow 的话就比较简单了。因为 StateFlow 就是 SharedFlow 的一种特殊子类,特点有三:

1)它的 replay cache 容量为 1;即可缓存最近的一次粘性事件;

2)初始化时必须给它设置一个初始值;

3)每次发送数据都会与上次缓存的数据作比较,如果不一样才会发送,自动过滤掉没有发生变化的数据。

它还可直接访问它自己的 value 参数获取当前结果值,总体来说,在使用上与 LiveData 相似,下面是它俩的异同点对比。


2.1 与 LiveData 比较的相同点


  1. 均提供了 可读可写 和 仅可读 两个版本:MutableStateFlow、StateFlow 与 MutableLiveData、LiveData;
  2. 允许被多个观察者观察,即生产者对消费者可以为一对多的关系;
  3. 都只会把最新的值给到观察者,即使没有观察者,也会更新自己的值;
  4. 都会产生粘性事件问题;
  5. 都可能产生丢失值的问题;

粘性事件问题:因为 StateFlow 初始化时必须给定初始值,且 replay 为 1,所以每个观察者进行观察时,都会收到最近一次的回播数据。如果想避免粘性事件问题,换用 SharedFlow 即可,replay 使用默认值 0 。

值丢失问题:出现在消费者处理数据比生产者生产数据慢的情况,消费者来不及处理数据,就会把之前生产者发送的旧数据丢弃掉,看个例子:

//code 11
    private fun stateFlowDemo1() {
        val stateFlow = MutableStateFlow(0)
        CoroutineScope(Dispatchers.Default).launch {
            var count = 1
            while (true) {
                val tmp = count++
                delay(1000)
                println("+++++ tmp = $tmp")
                stateFlow.value = tmp
            }
        }
        CoroutineScope(Dispatchers.Default).launch {
            stateFlow.collect{
                println("++++ count = $it")
                delay(5000)  //模拟耗时操作
            }
        }
    }

image.png

可以从打印结果看出,StateFlow 会丢弃掉生产者之前发送的值,其实 MutableStateFlow 的丢弃策略就是设置的 BufferOverflow.DROP_OLDEST。


2.2 与 LiveData 比较的不同点


  1. StateFlow 必须在构建的时候传入初始值,LiveData 不需要;
  2. StateFlow 默认是防抖的,LiveData 默认不防抖;
  3. 对于 Android 来说 StateFlow 默认没有和生命周期绑定,直接使用会有问题;

StateFlow 默认防抖:即如果发送的值与上次相同,则生产者并不会真正发送。在源码中也有说明,具体在 StateFlow.kt -> class StateFlowImpl -> private fun updateState -> if (oldState == newState) return true感兴趣的可以自行查阅,我看的版本是 1.5.0.

与 LiveData 相比,没有和 Activity 的生命周期绑定恐怕是使用 StateFlow 最不方便的地方了。当 View 进入 STOPPED 状态时,LiveData.observe() 会自动取消注册使用方,这样就不会再接收到数据了,也符合常理。因为用户此时已经离开页面,再接收数据已没有意义,如果继续处理后续逻辑可能还会出 bug。

而如果使用的是 StateFlow 或其他数据流,在 View 进入 STOPPED 状态时,收集数据的操作并不会自动停止。如需实现相同的行为,则需要从 Lifecycle.repeatOnLifecycle 块收集数据流。如下是来自官方文档的例子:

//code 12
class LatestNewsActivity : AppCompatActivity() {
    private val latestNewsViewModel = // getViewModel()
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Start a coroutine in the lifecycle scope
        lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // Note that this happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                latestNewsViewModel.uiState.collect { uiState ->
                    // New value received
                    when (uiState) {
                        is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                        is LatestNewsUiState.Error -> showError(uiState.exception)
                    }
                }
            }
        }
    }
}
//注意:repeatOnLifecycle API 仅在 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 库及更高版本中提供。

英文部分注释说的比较明确了,repeatOnLifecycle(Lifecycle.State.STARTED) 的作用就是每次进入 STARTED 可见状态时都会重新观察并收集数据;而在 STOPPED 状态时就会 cancel 掉 StateFlow 收集流所在的协程从而停止收集。


总结


最后总结一下 Flow 第二小节的内容吧:

1)热流有无消费者都可发送数据,生产者和消费者的关系可以是一对多;

2)SharedFlow 可构建热流,可设置 replay 重播数据量及 extraBufferCapacity 缓冲区大小,以及 onBufferOverflow 缓冲区满的策略;

3)emittryEmit 发送方法的异同,前者是挂起函数,注意在使用默认构造的 SharedFlow 时不要使用 tryEmit

4)StateFlow 是 SharedFlow 的一个子类,replay = 1,必须给定初始值,自带防抖;

5)使用 StateFlow 或 SharedFlow 收集值时,记得在 repeatOnLifecycle(Lifecycle.State.STARTED) 方法中,防止出现崩溃等问题。

更多内容,欢迎关注公众号:修之竹

赞人玫瑰,手留余香!欢迎点赞、转发~ 转发请注明出处~


参考文献


  1. Reactive Streams on Kotlin: SharedFlow and StateFlow; Ricardo Costeira; https://www.raywenderlich.com/22030171-reactive-streams-on-kotlin-sharedflow-and-stateflow
  2. Kotlin中 Flow、SharedFlow与StateFlow区别;五问;https://juejin.cn/post/7142038525997744141
  3. 一看就懂!图解 Kotlin SharedFlow 缓存系统;fundroid;https://juejin.cn/post/7156408785886511111
  4. Kotlin:深入理解StateFlow与SharedFlow,StateFlow和LiveData使用差异区分,SharedFlow实现源码解析;  pumpkin的玄学;  https://blog.csdn.net/weixin_44235109/article/details/121594988?spm=1001.2014.3001.5502
  5. StateFlow 和 SharedFlow 官方文档  https://developer.android.google.cn/kotlin/flow/stateflow-and-sharedflow?hl=zh-cn
目录
相关文章
|
2月前
|
移动开发 安全 Android开发
构建高效Android应用:Kotlin协程的实践与优化策略
【5月更文挑战第30天】 在移动开发领域,性能优化始终是关键议题之一。特别是对于Android开发者来说,如何在保证应用流畅性的同时,提升代码的执行效率,已成为不断探索的主题。近年来,Kotlin语言凭借其简洁、安全和实用的特性,在Android开发中得到了广泛的应用。其中,Kotlin协程作为一种新的并发处理机制,为编写异步、非阻塞性的代码提供了强大工具。本文将深入探讨Kotlin协程在Android开发中的应用实践,以及如何通过协程优化应用性能,帮助开发者构建更高效的Android应用。
|
14天前
|
消息中间件 缓存 API
Kotlin中的StateFlow和SharedFlow有什么区别?
- `StateFlow`持有一个最新的状态值,适合UI状态管理,新订阅者立即收到当前值。 - `SharedFlow`是通用热流,用于事件总线,不保留最新状态但可配置重播。 - `StateFlow`继承自`SharedFlow`,更专注于状态管理,而`SharedFlow`提供事件处理灵活性。 - 示例中展示了如何`emit`新值和`collect`变化。 - 选择`StateFlow`用于单最新状态共享,`SharedFlow`则适用于需要事件历史或定制策略的场景。 关注公众号“AntDream”了解更多内容!
20 1
|
2天前
|
安全 Kotlin
Kotlin中的安全导航操作符?.、空合并运算符?:以及let函数的实践与理解
Kotlin中的安全导航操作符?.、空合并运算符?:以及let函数的实践与理解
3 0
|
2月前
|
API 调度 Android开发
打造高效Android应用:探究Kotlin协程的优势与实践
【5月更文挑战第27天】在移动开发领域,性能优化和响应速度是衡量应用质量的关键因素。随着Kotlin语言的普及,协程作为其核心特性之一,为Android开发者提供了一种全新的并发处理方式。本文深入探讨了Kotlin协程在Android应用开发中的优势,并通过实例演示如何在实际项目中有效利用协程提升应用性能和用户体验。
|
2月前
|
移动开发 Android开发 开发者
构建高效Android应用:探究Kotlin协程的优势与实践
【5月更文挑战第21天】在移动开发领域,性能优化和流畅的用户体验是至关重要的。随着Kotlin语言在Android平台的广泛采纳,其并发处理的强大工具—协程(Coroutines),已成为提升应用响应性和效率的关键因素。本文将深入分析Kotlin协程的核心原理,探讨其在Android开发中的优势,并通过实例演示如何有效利用协程来优化应用性能,打造更加流畅的用户体验。
30 4
|
2月前
|
物联网 区块链 Android开发
构建高效Android应用:Kotlin与Jetpack的实践之路未来技术的融合潮流:区块链、物联网与虚拟现实的交汇点
【5月更文挑战第30天】 在移动开发领域,效率和性能始终是开发者追求的核心。随着技术的不断进步,Kotlin语言以其简洁性和现代化特性成为Android开发的新宠。与此同时,Jetpack组件为应用开发提供了一套经过实践检验的库、工具和指南,旨在简化复杂任务并帮助提高应用质量。本文将深入探索如何通过Kotlin结合Jetpack组件来构建一个既高效又稳定的Android应用,并分享在此过程中的最佳实践和常见陷阱。
|
2月前
|
运维 监控 Android开发
构建高效自动化运维系统的策略与实践构建高效Android应用:Kotlin协程的实践指南
【5月更文挑战第29天】随着信息技术的迅猛发展,企业IT基础设施变得日益复杂,传统的手动运维模式已难以满足高效率、高稳定性的要求。本文将深入探讨如何通过自动化工具和策略来构建一个高效的自动化运维系统。文中不仅分析了自动化运维的必要性,还详细介绍了实现过程中的关键步骤,包括监控、配置管理、故障响应等,并结合实际案例分析其效果,以期为读者提供一套行之有效的自动化运维解决方案。
|
2月前
|
移动开发 数据库 Android开发
构建高效Android应用:探究Kotlin协程的优势与实践
【5月更文挑战第29天】 随着移动开发技术的不断进步,开发者寻求更高效、更简洁的方式来编写代码。在Android平台上,Kotlin语言凭借其现代化的特性和对协程的原生支持,成为提高开发效率的关键。本文将深入分析Kotlin协程的核心优势,并通过实例展示如何在Android应用开发中有效地利用协程来处理异步任务,优化性能,以及提升用户体验。通过对比传统线程和回调机制,我们将揭示协程如何简化异步编程模型,并减少内存消耗,使应用更加健壮和可维护。
|
2月前
|
移动开发 安全 编译器
构建高效Android应用:探究Kotlin协程的优势与实践
【5月更文挑战第27天】 在移动开发领域,性能优化和流畅的用户体验始终是开发者追求的目标。随着Android对Kotlin的支持日益增强,Kotlin协程作为一种新的并发处理方式,为Android应用的性能提升提供了新的可能性。本文将深入探讨Kotlin协程的核心优势,并通过具体实例展示如何在Android应用中有效利用协程来提升响应速度、减少内存消耗,并简化异步代码。
|
2月前
|
存储 缓存 算法
深入理解操作系统内存管理:分页系统的优势与挑战构建高效Android应用:探究Kotlin协程的优势与实践
【5月更文挑战第27天】 在现代计算机系统中,内存管理是操作系统的核心功能之一。分页系统作为一种内存管理技术,通过将物理内存划分为固定大小的单元——页面,为每个运行的程序提供独立的虚拟地址空间。这种机制不仅提高了内存的使用效率,还为多任务环境提供了必要的隔离性。然而,分页系统的实现也带来了一系列的挑战,包括页面置换算法的选择、内存抖动问题以及TLB(Translation Lookaside Buffer)的管理等。本文旨在探讨分页系统的原理、优势及其面临的挑战,并通过分析现有解决方案,提出可能的改进措施。