👀【Flow】图文详解Kotlin中SharedFlow和StateFlow

在进入StateFlow和SharedFlow学习之前,我们先回顾下Flow的基本知识,在之前的文章中有介绍过,Flow是一种冷流,只有在收集者开始收集数据的时候,它才会去发射数据,而今天介绍的StateFlow和SharedFlow却是一种热流,只要有数据它们就会发射并不会在意有没有收集者。

文章内容较长,请大家慢慢食用🤣

【Kotlin】Flow流知识梳理

【Kotlin】Kotlin中Flow操作符解析大全

先来看看它们三者的对应关系,如下图所示:

从上面的关系图可以看出,SharedFlow和StateFlow都是Flow的子类,并且StateFlow也是SharedFlow的直接子类,可以理解为状态流也是特殊的一种共享流,那么我们就先从SharedFlow入手,先把它的使用和原理弄清楚之后,那么StateFlow就很轻松的理解了。

SharedFlow

在日常开发中,通常使用MutableSharedFlow()方法创建一个SharedFlow对象

此方法有三个参数,我们先看下这三个参数定义和作用:

  • replay:回放参数,此参数的作用就是在收集时,可以先收集到已经发射过的某些数量值,默认为0
  • extraBufferCapacity:额外的缓冲容量,此参数可以定义除了指定回放数量以外的可缓冲数,默认为0
  • onBufferOverflow:处理缓冲的策略,这个策略配置的是发射数据时处理缓冲区数据溢出的方式,BufferOverFlow是一个枚举类,内部有三种模式分别为SUSPEND、DROP_OLDEST和DROP_LATEST,这里先简单介绍下三种模式的大概意思
    • SUSPEND表示当缓冲区数据满了的时候,会将发射事件挂起
    • DROP_OLDEST表示当缓存区数据满了的时候,再次发送数据时会舍弃缓冲区最旧的数据
    • DROP_LATEST表示当缓存区数据满了的时候,再次发送数据时会舍弃缓存区最新的数据

这里先大致了解下这三种模式,后面会通过代码示例详细解释下。下面我们先最简单使用下SharedFlow,参数都采用默认的值,看看效果如何。

默认参数的共享流

kotlin 复制代码
fun main(): Unit = runBlocking {
    sampleShared()
}

private suspend fun sampleShared() {
    val sharedFlow = MutableSharedFlow<Int>()
    GlobalScope.launch {
        for (i in 0..10) {
            println("shareFlow emit: $i")
            sharedFlow.emit(i)
            delay(100L)
        }
    }
    GlobalScope.launch {
        sharedFlow.collect {
            println("sharedFlow collect: $it")
        }
    }
    delay(2000L)
}

上面使用MutableSharedFlow()方法创建了一个SharedFlow对象,然后开启两个协程,分别用于发送数据和接收数据互不干扰,发送11次数据,每次延时100ms,最后的delay(2000L)是为了能让上面协程完整执行完。最后我们看下运行后的效果

yaml 复制代码
log:
shareFlow emit: 0
shareFlow emit: 1
sharedFlow collect: 1
shareFlow emit: 2
sharedFlow collect: 2
shareFlow emit: 3
sharedFlow collect: 3
shareFlow emit: 4
sharedFlow collect: 4
shareFlow emit: 5
sharedFlow collect: 5
shareFlow emit: 6
sharedFlow collect: 6
shareFlow emit: 7
sharedFlow collect: 7
shareFlow emit: 8
sharedFlow collect: 8
shareFlow emit: 9
sharedFlow collect: 9
shareFlow emit: 10
sharedFlow collect: 10

我们对照上面的日志来分析下SharedFlow的运行机制,emit()是从0-10一共发送了11次数据,然而收集者却是从1开始收到数据,这是因为在发送数据0的时候,收集者还没开始工作,只有等到发送数据1的时候,收集者才真正的执行收集动作,此后就会按照发送的数据依次收集到数据。

从这里就可以看出SharedFlow是一个热流,在流的第一篇文章我们得知Flow是一个冷流,只有在收集者工作的时候才会发射数据,而从这里的数据0发射情况来看,即使收集者还没有工作的时候SharedFlow也会执行发送动作,热流不会在乎发射的时候有没有收集者在工作。

介绍完采用默认参数的SharedFlow之后,下面我们依次体会下SharedFlow三个参数的效果。

带回放参数的共享流

kotlin 复制代码
fun main(): Unit = runBlocking {
    sampleReplayShared()
}

private suspend fun sampleReplayShared() {
    val sharedFlow = MutableSharedFlow<Int>(replay = 2)
    GlobalScope.launch {
        for (i in 0..10) {
            sharedFlow.emit(i)
            delay(100L)
        }
    }
    // 这里延时1s,上面数据已经发送了0,1,2,3,4,5,6,7,8,9
    // 此时收集者还没开始工作,如果没有回放参数只能从9开始收集
    delay(1000L)
    GlobalScope.launch {
        sharedFlow.collect {
            println("sharedFlow collect: $it")
        }
    }
    delay(2000L)
}

这次创建MutableSharedFlow的时候,传入了reply = 2的参数,执行它的回放参数为2,并且在收集者工作之前延时1s,先让SharedFlow发送一会数据,延时过后再开始收集工作,1s的延时已经发送了数据0-9,如果回放参数采用默认0的话,此时收集者只能收集到数据10,那么运行下看看传入回放参数2的效果是怎样的呢?

sql 复制代码
log:
sharedFlow collect: 8
sharedFlow collect: 9
sharedFlow collect: 10

😲日志中竟然额外输出了8和9,从这也就可以看出,回放参数直接影响了收集数据的起始位置,即使数据在收集之前已经发送过了,收集者还是可以根据回放参数来收集到收集前发送的数据。

这里我们还可以通过ShareFlow.replayCache来更直观的观察到回放数据的变化,下面我们在每次发送数据之前打印一下replayCache值(它是一个列表对象,定义在ShareFlow接口中)

scss 复制代码
GlobalScope.launch {
    for (i in 0..10) {
        sharedFlow.emit(i)
        println("shareFlow emit: $i")
        val replayCache = sharedFlow.replayCache
        println("shareFlow replayCache: ${replayCache.joinToString()}")
        delay(100L)
    }
}

log:
shareFlow emit: 0
shareFlow replayCache: 0
shareFlow emit: 1
shareFlow replayCache: 0, 1
shareFlow emit: 2
shareFlow replayCache: 1, 2
shareFlow emit: 3
shareFlow replayCache: 2, 3
shareFlow emit: 4
shareFlow replayCache: 3, 4
shareFlow emit: 5
shareFlow replayCache: 4, 5
shareFlow emit: 6
shareFlow replayCache: 5, 6
shareFlow emit: 7
shareFlow replayCache: 6, 7
shareFlow emit: 8
shareFlow replayCache: 7, 8
shareFlow emit: 9
shareFlow replayCache: 8, 9
sharedFlow collect: 8
sharedFlow collect: 9
shareFlow emit: 10
shareFlow replayCache: 9, 10
sharedFlow collect: 10

上面的日志可能有点多,但是细看输出的顺序还是比较清晰的,当发送数据0的时候,此时回放缓存replayCache为0,发送数据1的时候回放缓存就变成了0和1,此后的回放缓存就依次增加,在发送完数据9的时候回放缓存为8和9,此时收集者开始工作,它会直接从回放缓存中取到8和9并顺利的收集过来,从日志中也可以看出shareFlow collect:8和shareFlow collect:9被打印出来,最后发送数据10的时候,收集者也就立即收集到并打印出来。

从这里示例就可以非常清楚的了解到replayCache的作用,它就是为了让收集者可以收集到在自己正式工作之前已经发送的数据,下面我们接着来看下extraBufferCapacity参数的效果和作用。

带额外缓冲容量和缓冲策略的共享流

这里直接将extraBufferCapacity和onBufferOverflow放在一起解释,它们俩都是用来处理缓冲数据的策略参数

挂起策略

kotlin 复制代码
private suspend fun sampleBufferCapacityShared() {
    val sharedFlow = MutableSharedFlow<Int>(
        extraBufferCapacity = 0,
        onBufferOverflow = BufferOverflow.SUSPEND
    )
    val startTime = System.currentTimeMillis()
    GlobalScope.launch {
        sharedFlow.collect {
            val currentTimeMillis = System.currentTimeMillis()
            println("sharedFlow ${currentTimeMillis - startTime} collect: $it")
            delay(300L)
        }
    }
    GlobalScope.launch {
        for (i in 0..10) {
            val currentTimeMillis = System.currentTimeMillis()
            println("shareFlow ${currentTimeMillis - startTime} emit: $i")
            sharedFlow.emit(i)
            delay(100L)
        }
    }
    delay(5000L)
}

上面将extraBufferCapacity和onBufferOverflow分别设置为0和SUSPEND,也就是表示缓冲区大小为0,发射的时候没有缓冲区可以存储只能挂起发送事件等待收集者处理完数据才可以进行下一次的数据发送,并且我们在收集数据的时候延时300ms来模拟耗时操作,而发送数据只有100ms的延时,这里故意将收集处的延时设置的比发送延时要长一点,这样就可以很清晰的看出ShareFlow是如何处理积压的发射数据,下面我们运行下代码看看实际情况是否如此。

yaml 复制代码
log:

shareFlow 11 emit: 0
sharedFlow 21 collect: 0
shareFlow 122 emit: 1
sharedFlow 325 collect: 1
shareFlow 426 emit: 2
sharedFlow 627 collect: 2
shareFlow 730 emit: 3
sharedFlow 928 collect: 3
shareFlow 1033 emit: 4
sharedFlow 1229 collect: 4
shareFlow 1329 emit: 5
sharedFlow 1532 collect: 5
shareFlow 1637 emit: 6
sharedFlow 1833 collect: 6
shareFlow 1938 emit: 7
sharedFlow 2134 collect: 7
shareFlow 2239 emit: 8
sharedFlow 2437 collect: 8
shareFlow 2538 emit: 9
sharedFlow 2740 collect: 9
shareFlow 2846 emit: 10
sharedFlow 3045 collect: 10

通过日志中emit和collect打印顺序可以看出,即使发送的延时低于收集的延时,它也不会按照既定的100ms发送一次数据来正常发送,它会在收集处延时完了之后才进行下一次的发送事件。

接着我们将extraBufferCapacity设置为2看看效果

yaml 复制代码
log:

shareFlow 11 emit: 0
sharedFlow 14 collect: 0
shareFlow 118 emit: 1
shareFlow 221 emit: 2
sharedFlow 319 collect: 1
shareFlow 322 emit: 3
shareFlow 427 emit: 4
sharedFlow 624 collect: 2
shareFlow 727 emit: 5
sharedFlow 927 collect: 3
shareFlow 1031 emit: 6
sharedFlow 1232 collect: 4
shareFlow 1336 emit: 7
sharedFlow 1532 collect: 5
shareFlow 1636 emit: 8
sharedFlow 1837 collect: 6
shareFlow 1941 emit: 9
sharedFlow 2138 collect: 7
shareFlow 2242 emit: 10
sharedFlow 2442 collect: 8
sharedFlow 2746 collect: 9
sharedFlow 3051 collect: 10

我们将上面的日志转换成流程图来直观的感受下整体过程的运作

对照着上面的流程图,我们一步一步的分析下整体流程:

  • 当发送数据0的时候,此时缓冲区是没有数据的,SharedFlow可以正常发送数据0,接收也是正常接收,发送完之后缓冲区不变;
  • 100ms之后又开始发送数据1,但是此时收集者还在300ms的延时内,没有收集者处理数据,只能将发送的数据存到缓冲区,那么此时缓冲区就变成数据1;
  • 200ns之后又开始发送数据2,此时依旧没有收集者收集数据,只能将数据2存到缓冲区,此时的缓冲区已经有数据1了,我们设置的缓冲容量为2,缓冲区新增数据2;
  • 300ms之后收集者延时已经结束,它可以继续接收数据了,由于缓冲区是有数据1和2的,那么此时优先接收缓冲区的数据,按顺序就接收了数据1,然后SharedFlow继续开始发送数据3,此时就会将数据3添加到缓冲区移除数据1,缓冲区数据变为2和3;
  • 400ms的时候是一个节点,此时SharedFlow接收处处于延时状态中,而发送处开始发送数据4,由于此时缓冲区已满,发送处就会变成挂起状态,等待收集处延时结束,缓冲区数据不变
  • 500ms时,SharedFlow接收处还是处于延时状态,发送处也只能处于挂起状态,此时发送和接收都不会有任何动作;
  • 600ms时,SharedFlow接收处延时状态结束,开始接收新的数据,优先接收缓冲区数据2,数据2被接收之后,发送处挂起状态也随之结束,就可以将挂起的数据4给发送出来,添加到缓冲区当中;
  • 600ms到2200ms之间的流程就省略不说了,流程类似,下一个主要节点就是2200ms;
  • 2200ms时,SharedFlow发送处开始发送最后一个数据10,此时接收处是延时状态,发送处不可以及时的将数据10发送出去,只能被迫挂起;
  • 2400ms之后,SharedFlow开始接收新的数据,它从缓冲区接收完数据8之后,发送处也将数据10发送出来,添加到缓冲区当中,此时缓冲区数据变为9和10,注意到此为止,已经没有新的数据继续发送,只需等待接收处每隔300ms接收新数据即可
  • 2700ms之后,SharedFlow开始依次从缓冲区接收新的数据,直到将数据9和10接收完毕。

上面就是整个extraBufferCapacity = 2和onBufferOverflow = SUSPEND的整体流程,总体来说就是挂起策略可以在缓冲区已满的情况下挂起发送事件,等待接收处处理完成之后继续发送新的数据。

接下来我们再看看onBufferOverflow = DROP_OLDEST和onBufferOverflow = DROP_LATEST的效果(缓冲区容量依旧设置为2)

丢弃缓冲区旧数据策略

DROP_OLDEST顾名思义就是丢弃最旧的数据,那么丢弃的是哪里旧数据呢?通过代码来理解下,示例代码还是沿用上面的,只是将onBufferOverflow修改为DROP_OLDEST,运行代码看看日志输出情况:

yaml 复制代码
log:

shareFlow 12 emit: 0
sharedFlow 16 collect: 0
shareFlow 121 emit: 1
shareFlow 225 emit: 2
sharedFlow 318 collect: 1
shareFlow 326 emit: 3
shareFlow 426 emit: 4
shareFlow 528 emit: 5
sharedFlow 622 collect: 4
shareFlow 631 emit: 6
shareFlow 731 emit: 7
shareFlow 832 emit: 8
sharedFlow 922 collect: 7
shareFlow 932 emit: 9
shareFlow 1034 emit: 10
sharedFlow 1225 collect: 9
sharedFlow 1529 collect: 10

从日志中可以看出,整体消费时间只有SUSPEND策略的一半,发送处是将数据0-10都发送出来,但是收集处只收集到了0、1、4、7、9和10数据,中间有好几个数据没有正常接收到,下面我们还是将日志转换成对应的流程图看看整体运行情况。

在了解了SUSPEND策略之后,DROP_OLDEST流程理解起来就很轻松了,下面还是一步一步的解释下整个流程:

  • 0ms时,SharedFlow开始发送数据0,接收处也开始接收数据0,此时缓冲区没有数据新增,接收处开始执行延时操作;
  • 100ms时,SharedFlow发送数据1,此时接收处还处于延时状态,发送的数据1只能暂时存到缓冲区中,缓冲区数据变为1;
  • 200ms时,SharedFlow发送数据2,此时接收处依旧处于延时状态,发送的数据2依然暂存到缓冲区中,缓冲区数据变为1和2;
  • 300ms时,接收处延时结束,开始收集新的数据,优先从缓冲区中获取数据,获取到数据1,此时发送处也开始发送新的数据3,由于缓冲区数据1被接收,数据3就可以暂存进去,缓冲区数据变为2和3;
  • 400ms时,SharedFlow发送数据4,接收处前面接收完数据1开始延时,只能将数据4存入缓冲区,但是此时缓冲区已经有2和3,达到了最大的容量,由于我们设置的是DROP_OLDEST策略,它会将缓冲区最旧的数据2丢弃,将发送的数据4存入缓冲区,缓冲区数据就变成3和4;
  • 中间逻辑一致,省略...(主要是想偷个懒😄)
  • 当1000ms时,SharedFlow开始发送最后一个数据10,此时发送处还在延时状态,就将数据10存入缓冲区,舍弃掉缓冲区最旧的数据8,缓冲区数据变为9和10;
  • 1000ms之后,SharedFlow已经没有数据可以发送,现在只需要等待接收处每隔300ms从缓冲区接收数据即可,直到将9和10接收完成。

从上面的流程可以看出,DROP_OLDEST在缓冲区数据已满的情况下,发送新的数据会将缓冲区最旧的那个数据丢弃,存入发送的数据,这也导致了接收者不能完整的接收所有发送过的数据。

丢弃缓冲区最新数据策略

DROP_LATEST的意思是丢弃最新的数据,它和DROP_OLDEST正好相反,此策略下缓冲区已满的情况下,发送新数据时缓冲区的数据是不变的,新发送的数据不会存入缓冲区中,直接丢弃。

我们在原有代码中onBufferOverflow修改为DROP_LATEST,运行一下看看日志输出情况:

yaml 复制代码
log:

shareFlow 11 emit: 0
sharedFlow 14 collect: 0
shareFlow 119 emit: 1
shareFlow 221 emit: 2
sharedFlow 316 collect: 1
shareFlow 322 emit: 3
shareFlow 426 emit: 4
shareFlow 529 emit: 5
sharedFlow 617 collect: 2
shareFlow 631 emit: 6
shareFlow 731 emit: 7
shareFlow 832 emit: 8
sharedFlow 918 collect: 3
shareFlow 934 emit: 9
shareFlow 1036 emit: 10
sharedFlow 1221 collect: 6
sharedFlow 1522 collect: 9

从collect的日志情况看下来,好像和我们预期的效果有点出入,在运行代码之前预期的只会收集到0、1、2、3、4、5,但是实际情况确实收集到了0、1、2、3、6、9这五个数据,有点懵,还是通过流程图分析下整体运行情况吧。

  • 0ms时,SharedFlow开始发送数据0,接收处也开始接收数据0,此时缓冲区没有数据新增,接收处开始执行延时操作;
  • 100ms时,SharedFlow发送数据1,此时接收处还处于延时状态,发送的数据1只能暂时存到缓冲区中,缓冲区数据变为1;
  • 200ms时,SharedFlow发送数据2,此时接收处依旧处于延时状态,发送的数据2依然暂存到缓冲区中,缓冲区数据变为1和2;
  • 300ms时,接收处延时结束,开始收集新的数据,优先从缓冲区中获取数据,获取到数据1,此时发送处也开始发送新的数据3,由于缓冲区数据1被接收,数据3就可以暂存进去,缓冲区数据变为2和3;
  • 400ms时就开始DROP_LATEST策略就开始发挥它的作用了,此时缓冲区数据已满,发送的数据4不可以进入缓冲区替换掉数据2,只能将新数据4丢弃掉,缓冲区数据不变依然为2和3;
  • 500ms时和前面400ms发生的事件一致,缓冲区数据依旧不变;
  • 600ms时,收集处开始工作从缓冲区接收数据2,发送处也开始发送新的数据6,此时缓冲区数据只有3,那新的数据6就可以暂存到缓冲区中,缓冲区数据变为3和6;
  • 中间省略一段,逻辑和之前类似
  • 当达到1000ms时,发送最后一个数据10,此时缓冲区已满数据10不可以存入到缓冲区,舍弃数据10,缓冲区数据依旧为6和9;
  • 后面的1200ms和1500ms,收集处开始缓冲区依次获取数据6和9,最终在1500ms接收结束,整个流程也就完成了。

从上面的流程可以看出,DROP_LATEST会在缓冲区已满的时候直接舍弃掉即将发送的数据,这就是它处理缓冲区的策略。到这为止整个SharedFlow机制就介绍完了,它的运行机制及三个参数的作用都在上面进行了详细的介绍,大家可以复制示例代码运行感受下它的工作流程,加深下对它的理解。下面进入StateFlow的相关知识介绍。

StateFlow

StateFlow在文章开始的地方就介绍到它是一种特殊的SharedFlow,看完上面对SharedFlow的解释之后,理解StateFlow就变得非常简单,下面直接看如何使用StateFlow。

kotlin 复制代码
fun main(): Unit = runBlocking {
    sampleStateFlow()
}

private suspend fun sampleStateFlow() {
    val stateFlow = MutableStateFlow(0)
    GlobalScope.launch {
        stateFlow.collect {
            println("stateFlow collect: $it")
        }
    }
    delay(100L)
    GlobalScope.launch {
        for (i in 1..10) {
            println("stateFlow emit: $i")
            stateFlow.emit(i)
            delay(100L)
        }
    }
    delay(2000L)
}

StateFlow通常由MutableStateFlow(value)形式创建,它必须传入一个value参数,表示StateFlow的默认值,即使后续没有数据发送,也可以收集到此默认值。

代码12行处延时100ms就是为了让收集者可以完成默认值0的过程,然后在发射的地方每次发送完延时100ms,接下来看看日志的输出情况

yaml 复制代码
stateFlow collect: 0
stateFlow emit: 1
stateFlow collect: 1
stateFlow emit: 2
stateFlow collect: 2
stateFlow emit: 3
stateFlow collect: 3
stateFlow emit: 4
stateFlow collect: 4
stateFlow emit: 5
stateFlow collect: 5
stateFlow emit: 6
stateFlow collect: 6
stateFlow emit: 7
stateFlow collect: 7
stateFlow emit: 8
stateFlow collect: 8
stateFlow emit: 9
stateFlow collect: 9
stateFlow emit: 10
stateFlow collect: 10

从上面可以看出收集者在发送新数据之前优先拿到默认数据0,后续每次发送新的数据都会正常的接收到,默认值就是它和SharedFlow的不同之处。

MutableStateFlow(value)最终会创建一个StateFlowImpl对象,它内部维护着_state的atomic对象,此对象的初始值就是我们传入的value,并且每次发送新数据的时候它也会随着改变成最新的值,即使在屏幕旋转之后,再次执行收集动作还是会拿到_state最新的数据。所以在开发中StateFlow适用于存储和界面相关的数据,这样在屏幕旋转或者activity意外重建的情况下也能保证界面之前的信息不会丢失,而SharedFlow适用于弹Toast、Dialog等场景,它不需要保存状态,就是执行一次性动作。

好了,到这里SharedFlow和StateFlow大致都介绍完了,如果你对文章有不同的见解或者疑问都可以在评论区交流,我也会及时回复,谢谢大家的阅读。

关于我

我是Taonce,如果觉得本文对你有所帮助,帮忙关注、赞或者收藏三连一下,谢谢🙂~

相关推荐
Devil枫2 小时前
Kotlin高级特性深度解析
android·开发语言·kotlin
ChinaDragonDreamer2 小时前
Kotlin:2.1.20 的新特性
android·开发语言·kotlin
雨白12 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹14 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空16 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭16 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日17 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安17 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑17 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟21 小时前
CTF Web的数组巧用
android