Flow
众所周知,Flow 是一个数据流工具,最上游是数据的生产者(本质上是一系列数据的生产规则),每当下游开始调用 Flow.collect()
,数据的生产线就开始启动,根据生产规则将数据一个个生产出来,往下流动,最终到达收集处被消费处理。
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val flow: Flow<Int> = flow {
println("[flow]: start")
(1..3).forEach {
delay(1000)
emit(it)
}
}
launch {
println("[collector A]: start")
flow.collect {
println("[collector A]: $it")
}
}
launch {
println("[collector B]: start")
flow.collect {
println("[collector B]: $it")
}
}
}
/*
[collector A]: start
[collector B]: start
[flow]: start
[flow]: start
[collector A]: 1
[collector B]: 1
[collector A]: 2
[collector B]: 2
[collector A]: 3
[collector B]: 3
*/
上面的代码里,Flow 的生产规则是:每隔 1 秒生产一个数据,一共生产 3 个数据,分别是 1、2、3。由运行结果可知,数据是在调用 Flow.collect()
后才开始生产的,且每次调用 Flow.collect()
都会启动一个全新的生产流程。换句话说,collect()
函数的作用是启动数据的生产流程,并对产出的数据进行收集处理。
Flow.launchIn()
Flow 有一个拓展函数 Flow.launchIn()
,它的原理很简单,就是利用给定的 CoroutineScope 开启一个新协程,然后在这个新协程里面调用 Flow.collect()
。
// Collect.kt
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect()
}
但这里的 collect()
并没有对数据进行什么处理,没有打印、也不回调,启动数据生产流程,收集了产出的数据但不用?搞什么啊... 别忘了生产出来的数据可能会流经各个中间操作符,collect 只是数据的最后一站。launchIn()
一般是配合中间操作符 onEach()
一起使用的:
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val flow: Flow<Int> = flow {
(1..3).forEach {
delay(1000)
emit(it)
}
}
// this.launch {
// flow.collect {
// println(it)
// }
// }
// this.launch {
// flow
// .onEach { println(it) }
// .collect()
// }
// 等同于上面的两种写法
flow
.onEach { println(it) }
.launchIn(this)
}
SharedFlow
了解完 Flow 的生产-消费模式、collect()
、launchIn()
这些基础知识后,我们开始今天的主题——SharedFlow。
对使用过 Flow 的开发者而言,或多或少都对 SharedFlow 有所了解,知道它是一种事件流工具,能够作为 EventBus 的替代方案。下面这张图来自 EventBus 仓库的 README:
发布者将 event 事件 post 出去,所有订阅该该类型事件的订阅者都会收到通知(onEvent)。
我们可以将 EventBus 的 post 看作是 Flow 的 emit,将 Event 的 onEvent 看作是 Flow 的 collect,但很快就能发现不对劲,在 Flow 里面,每次调用 collect 都会启动一个新的生产流程,也就是说消费者和生产者是一一对应的。而在 EventBus 里面,事件发布者只有一个,某个事件被发送出来,多个订阅者能同时收到该事件,发布者和订阅者的关系是一对多。
用 Flow 怎么实现事件订阅呢?答案不言而喻,是 SharedFlow。
Flow.shareIn()
Flow 有一个拓展函数 Flow.shareIn()
用于将一个 Flow 转换为 SharedFlow:
// Share.kt
fun <T> Flow<T>.shareIn(
scope: CoroutineScope, // [Where] 指定在哪开启新协程调用 Flow.collect()
started: SharingStarted, // [When] 指定什么时候调用 Flow.collect()
replay: Int = 0
): SharedFlow<T>
返回的 SharedFlow 对象,会将上游 Flow 产出的每个数据作为事件转发给所有的 SharedFlow.collect()
收集者。
SharedFlow 官方文档里面写道:An active collector of a shared flow is calle a subscriber.
意思是普通 Flow 的 collector 称为(数据)收集者,但是 SharedFlow 的 collector 可以被视为订阅者。
![]()
下面的代码里,我们将一个现有的 Flow 使用 shareIn()
转换为 SharedFlow:
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val flow: Flow<Int> = flow {
println("[flow] start")
(1..3).forEach {
delay(1000)
println("[flow] emit $it")
emit(it)
}
}
val sharedFlow: SharedFlow<Int> = flow.shareIn(
scope = this,
started = SharingStarted.Eagerly,
)
delay(1500)
launch {
println("[collector A] start")
sharedFlow.collect { println("[collector A]: $it") }
}
launch {
println("[collector B] start")
sharedFlow.collect { println("[collector B]: $it") }
}
}
/*
[flow] start
[flow] emit 1
[collector A] start
[collector B] start
[flow] emit 2
[collector B]: 2
[collector A]: 2
[flow] emit 3
[collector A]: 3
[collector B]: 3
*/
我们给 shareIn()
传递了参数 SharingStarted.Eagerly
,Eagerly 是 "急切地、急迫地" 的意思,它会立刻调用上游 Flow 的 collect()
启动其生产流程,并将收集到的每个数据转发给所有收集者。第一条数据是在第 1 秒时被生产出来的,但是在 1.5 秒时才调用的 SharedFlow.collect()
,所以两个订阅者只收到了数据(事件) 2 和 3。
热的 SharedFlow
观察上面的运行结果,会发现 SharedFlow 是一种 Hot Flow(热流),而普通的 Flow 是一种 Cold Flow(冷流)。
何谓冷流🥶和热流🥵呢?冷热的概念来源于 Flow 的特性,它们描述了流的行为方式和数据的发出时机。
冷流指的是每次收集(collect)流时,数据生产线会从头开始执行、发出数据。称其为冷流是因为它的行为像是在冷藏的状态下,只有在“激活”它(即调用 collect)时,才开始从头运行并发出数据。
🥶 冷流的特点:
- 懒加载:数据的生产代码只会在开始收集时才开始执行。
- 冷流的数据生产和消费是一体的,每次调用 collect() 时,流会从头开始生产数据。
![]()
热流指的是流的数据是实时生成并广播给所有观察者的。也就是说,热流的生产者在流创建后就会生产并发射数据,而不管有没有人去收集它,热流的数据生产和消费是独立的。称其为热流是因为它的行为像是一个实时的广播,总是活跃的,像是一个正在加热的设备,不会因是否有人“收听”而改变其状态。
🥵热流的特点:
- 非懒加载:热流会立即开始生产并发出数据。
- 热流的数据生产和消费是独立的,多个收集者可以共享数据。
再看这张图,应该非常容易理解 SharedFlow 的“热”了:
虽然 SharedFlow 被 collect()
了多次,但它们都共享同一套数据源,而不是各自一套,这就是 SharedFlow 中的 Share 的意思,即共享。
永不结束的收集
SharedFlow 与普通 Flow 的 collect()
不同,它的收集并不会随着上游 Flow 的生产过程结束而一起结束:
suspend fun main(): Unit = coroutineScope {
val flow: Flow<Int> = flow {
(1..3).forEach {
delay(1000)
emit(it)
}
}
launch {
flow.collect {
println("[flow collector]: $it")
}
println("[flow collector] END")
}
launch { // this: CoroutineScope
val sharedFlow: SharedFlow<Int> = flow.shareIn(
scope = this,
started = SharingStarted.Eagerly,
)
sharedFlow.collect {
println("[shared flow collector]: $it")
}
println("[shared flow collector] END")
}
}
/*
[shared flow collector]: 1
[flow collector]: 1
[flow collector]: 2
[shared flow collector]: 2
[shared flow collector]: 3
[flow collector]: 3
[flow collector] END 👈 只有普通 Flow 的 collect 函数返回了
*/
即使数据全部生产完毕,SharedFlow.collect()
的下一行代码也得不到运行,因为它的收集流程是不会结束的。
点开 SharedFlow 的源码可以看到:
// Flow.kt
public interface Flow<out T> {
public suspend fun collect(collector: FlowCollector<T>) // 没写返回值类型,默认 Unit
}
// SharedFlow.kt
public interface SharedFlow<out T> : Flow<T> {
override suspend fun collect(collector: FlowCollector<T>): Nothing // 👈
}
它把 Flow 的 collect 函数重写了,返回类型是 Nothing,也就是说它要么永不返回,要么抛异常。
SharedFlow.collect()
虽然永远不会返回,但不代表它永远不会结束,collect()
终究是一个挂起函数,它必须在协程里面运行,如果协程被取消,collect()
函数自然就会被取消(结构化取消),因为 SharedFlow 的 collect()
函数做了交互式取消的工作:
// SharedFlow.kt
internal open class SharedFlowImpl<T>(
...
) : AbstractSharedFlow<SharedFlowSlot>(), MutableSharedFlow<T>, CancellableFlow<T>, FusibleFlow<T> {
override suspend fun collect(collector: FlowCollector<T>): Nothing {
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() // 👈 注意这里的 ensureActive
collector.emit(newValue as T)
}
} finally {
freeSlot(slot)
}
}
}
Flow.shareIn() 参数详解
// Share.kt
fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T>
scope: CoroutineScope
第一个参数需要填的是一个 CoroutineScope,跟 Flow.launchIn()
是一样的,它决定从哪里启动新协程,然后在这个新协程里去收集上游 Flow。
replay: Int
第三个参数,replay “重播“
在前面的例子里我们发现 SharedFlow 的 Subscriber (Collector) 是收不到它收集开始前流生产的数据的。
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val sharedFlow: SharedFlow<Int> = flow {
(1..3).forEach {
delay(1000)
emit(it)
}
}.shareIn(
scope = this,
started = SharingStarted.Eagerly,
replay = 0, // 默认值
)
launch {
delay(1500)
sharedFlow.collect {
println("collect1: $it")
}
}
launch {
delay(2500)
sharedFlow.collect {
println("collect2: $it")
}
}
}
// collect1: 2
// collect1: 3
// collect2: 3
比如上面的代码,数据 1、2、3 分别在第 1、2、3 秒被生产出来,但第一次收集是在 1.5 秒开始的,错过了数据 1,第二次收集是在 2.5 秒开始的,错过了数据 1 和 2。
参数 replay 指定的是 SharedFlow 的重放数量。
缓存
若将 replay 指定为 n(非负数,默认为 0),那么 SharedFlow 就会开启缓存,意味着它会缓存发射过的最近 n 个数据,在未来发送给新的 Subscriber,即“重放”。
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val sharedFlow: SharedFlow<Int> = flow {
(1..3).forEach {
delay(1000)
emit(it)
}
}.shareIn(
scope = this,
started = SharingStarted.Eagerly,
- replay = 0, // 默认值
+ replay = 1,
)
delay(1500)
sharedFlow.collect {
println("collect $it")
}
}
+ // collect1: 1
// collect1: 2
+ // collect2: 2
// collect1: 3
// collect2: 3
上图以 replay = 1 为例 :
- Emitter 发送 ❶ ,被 Buffer 缓存;
- Subscriber1 订阅 SharedFlow 后,接收到缓存的 ❶ 进行处理;
- Emitter 发送 ❷,替换掉 Buffer 里的 ❶,同时发给现有的 Subscriber1 处理;
- Subscriber2 订阅 SharedFlow 后,接收到缓存的 ❷ 进行处理;
- Emitter 发送 ❸,替换掉 Buffer 里的 ❷,同时发给现有的 Subscriber1 和 Subscriber2 处理;
Q1:为什么 buffer size = 64,不是说 replay 指定缓存多少个最近的数据吗?
A1:由Flow.shareIn()
创建出来的 SharedFlow,如果 replay < 64,则 buffe size = 64,如果 replay > 64,那么 buffer size = replay。
Q2:buffer 大小(64)完全够容纳两个数据,为什么 ❷ 进入 buffer 时要把 ❶ 替换掉?
A2:因为 replay = 1,只需要缓存一个最新的数据,用于在未来发给新的订阅者,将旧的 ❷ 留在 buffer 里面没有意义。
以上例子体现了 replay 的缓存功能:能将最近发射的数据缓存下来,发给新增的 Subscriber。
缓冲
在上面我们接触了 SharedFlow 的 buffer,buffer 配合 replay 起到了缓存的作用。其实 buffer 还有另一个作用:在下游来不及消费时缓冲上游的数据。
在生产者-消费者模型中,当生产速度超过消费速度时,系统需要采取措施加以控制,通常的做法是暂停生产或丢弃来不及消费的数据。这种现象在 SharedFlow 中同样存在:当 Subscriber 的处理效率较低时,Buffer 中的数据若无法及时处理,缓冲区总有一刻将被填满。默认情况下,buffer 被填满后数据发射操作会被挂起(onBufferOverflow = SUSPEND)。
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
flow<Int> {
val start = System.currentTimeMillis()
(1..100).forEach {
emit(it)
println(
"emit $it finished at " +
(System.currentTimeMillis() - start)
.toDuration(DurationUnit.MILLISECONDS)
.toString()
)
}
}.shareIn(
scope = this,
replay = 1,
started = SharingStarted.Eagerly,
).collect {
delay(3000)
}
}
上面的代码里,生产每条数据几乎不耗时,处理每条数据则要花费 3 秒。
- Emmiter 发送 1,进入 buffer 被缓存,同时发给 Subscriber 处理(耗时 3 秒);
- Emmiter 发送 2,替换掉 buffer 中的 1,替换掉是因为 replay = 1,只需保留最新的 1 条数据;
- Emmiter 发送 3,进入 buffer 被缓冲,它不能替换掉 2,因为 2 还没有被现有的所有 Subscriber 消费,还得留着 2,等 Subscriber 处理完 1 了,把 2 给它处理;
- ...
- Emmiter 发送 66,想要进入 buffer,此时 buffer 已经满了,于是生产流程只能停止(挂起);
- 直到第 3 秒,数据 1 被处理完毕,Subscriber 开始处理 buffer 中的 2,此时 2 被现有的所有 Subscriber 消费了,它不需要被缓冲了,同时,因为 replay = 1,2 不是最新那条数据,也没必要留着发送给未来的新订阅者。是时候将 2 逐出 buffer 了,腾出位置让门外的 66 进来。
- 生产流程得以继续,Emitter 发送 67,想要进入 buffer,此时 buffer 又已经满了,于是生产流程又只能停止(挂起)了......
注意 SharedFlow 作为一个多播可以有多个 Subscriber,上面例子中,数据 2 被消费的时间点,取决于最后一个开始处理它的 Subscriber。
为了学习 Flow.shareIn()
函数参数 replay,我们学了 SsharedFlow 的 buffer,知道它可以配合 replay“缓存”最新的数据,以便在未来发给新的订阅者。还知道了 buffer 可以在下游来不及消费时“缓冲”上游生产的数据。
started: SharingStarted
Flow.shareIn()
最后一个参数,SharingStarted,直译就是“共享开始的时间”,
// SharingStarted.kt
public fun interface SharingStarted {
public companion object {
public val Eagerly: SharingStarted = StartedEagerly()
public val Lazily: SharingStarted = StartedLazily()
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
): SharingStarted =
StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis)
}
}
SharingStarted.Eagerly
Eagerly 的意思是“急切地、急迫地”,也就是在调用 Flow.shareIn()
的时候就立马开始启动上游数据的生产。
SharingStarted.Lazily
lazy 就是懒的意思,填入 SharingStarted.Lazily
,那么在调用 Flow.shareIn()
的时候,不会立马开始启动上游 Flow 的生产流程,直到第一次调用 SharedFlow.collect()
。
SharingStarted.WhileSubscribed
第三种 SharingStarted 叫 WhileSubscribed,通过它可以不仅可以设置在第一次调用 SharedFlow.collect()
的时候启动上游 Flow 的生产,还可以配置上游 Flow 结束和重启的规则。
使用 SharingStarted.WhileSubscribed()
,当下游的所有订阅全都结束之后,它会把上游的所有的生产过程也结束掉,而且这时候如果再来新的订阅,它就会重新启动上游的数据流。
比如下面的代码:
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val flow = flow {
println("start emit")
(1..3).forEach {
delay(1000)
emit(it)
}
}
val sharedFlow = flow.shareIn(
scope = this,
started = SharingStarted.WhileSubscribed(),
replay = 1,
)
val job = launch {
println("collect1 start")
sharedFlow.collectIndexed { index, value ->
println("collect1: $value")
if (index == 1) cancel() // 收集完第二个数据后停止收集
}
}
job.join()
println("collect1 finished")
delay(1000)
launch {
println("collect2 start")
sharedFlow.collect {
println("collect2: $it")
}
}
}
第一次收集的时候,收集了两个数据就结束收集了,此时唯的一 collect 已经结束,所以上游的 Flow 生产流程也结束了,随后再次调用 SharedFlow.collect()
,上游 Flow 生产流程再次被启动了,所以打印了两次 "start emit".
请注意,第二次调用 SharedFlow.collect()
时,上游的生产还没被重启,就先打印了一条数据(2),这是因为参数 replay = 1,那是 SharedFlow 缓存下来的数据。
WhileSubscribed()
有两个参数,它们都是用于配置延时。
stopTimeoutMillis: Long
stopTimeoutMillis 用于配置结束的延时。具体来说,如果 SharedFlow 的所有 collect 都结束了,此时先不结束上游 Flow 的生产流程,从现在开始计时,如果计时过程中,有新的 SharedFlow.collect()
被调用,那么就不取消上游 Flow 的生产流程了。当然,如果计时结束了仍没有新的 SharedFlow.collect()
被调用,那么就会正式结束上游 Flow 的生产。
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val flow = flow {
println("start emit")
(1..3).forEach {
delay(1000)
emit(it)
}
}
val sharedFlow = flow.shareIn(
scope = this,
started = SharingStarted.WhileSubscribed(
+ stopTimeoutMillis = 1000,
),
replay = 1,
)
val job = launch {
println("collect1 start")
sharedFlow.collectIndexed { index, value ->
println("collect1: $value")
if (index == 1) cancel()
}
}
job.join()
println("collect1 finished")
- delay(1000)
+ delay(500)
launch {
println("collect2 start")
sharedFlow.collect {
println("collect2: $it")
}
}
}
在上一个例子的基础上,我们把代码改改,延迟 1 秒结束上游 Flow(stopTimeoutMillis = 1000
),在第一次收集结束后,隔 0.5 秒就启动二次收集。
"start emit" 只打印了一次,可见上游 Flow 确实没有重启。
replayExpirationMillis: Long = Long.MAX_VALUE
第二个参数, replayExpirationMillis,replay expiration millis 重播过期毫秒?
它设置的是缓存的失效时间。在上游 Flow 结束了,并且设置的 replayExpirationMillis
也超时之后(期间没有新的 SharedFlow.collect()
调用),SharedFlow 缓存下来的那些最近数据就会被清空。
replayExpirationMillis
的默认值是无限大,也就是永远不会在上游 Flow 结束后丢弃缓存的最近数据,如果设置成 0,它就是在上游 Flow 结束的一瞬间就清空缓存的最近数据。
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val flow = flow {
println("start emit")
(1..3).forEach {
delay(1000)
emit(it)
}
}
val sharedFlow = flow.shareIn(
scope = this,
started = SharingStarted.WhileSubscribed(
- stopTimeoutMillis = 1000,
+ stopTimeoutMillis = 0,
+ replayExpirationMillis = 250,
),
replay = 1,
)
val job = launch {
println("collect1 start")
sharedFlow.collectIndexed { index, value ->
println("collect1: $value")
if (index == 1) cancel()
}
}
job.join()
println("collect1 finished")
delay(500)
launch {
println("collect2 start")
sharedFlow.collect {
println("collect2: $it")
}
}
}
这次我们仍然在上个例子的基础上修改,所有 collect 结束收集时,立马停止上游 Flow 的生产,同时将缓存过期时间设置为 250 毫秒,由于两次收集间隔为 500 毫秒,所以缓存下来的数据 2 被提前清空了,并没有被发送给新的订阅者。
stopTimeoutMillis 和 replayExpirationMillis 两个参数都是配置延时,一个是对于结束 Flow 的延时,一个是 Flow 结束之后,清空缓存数据的延时。
如果程序里有一个可以被订阅的事件流,该事件流会在多个地方被订阅,同时这个事件流的生产流程非常消耗资源,需要在所有订阅都结束的时候,及时的结束 Flow 的生产,这种场景下就非常适合用
WhileSubscribed()
来配置这种自动结束自动重启的 SharedFlow 了。
SharedFlow 的使用场景
所谓深入浅出,最后总结一下 SharedFlow 的几个特性以及它的使用场景。
SharedFlow 是一种事件流工具,自然是适用于 "事件订阅" 的场景。为什么它适合事件订阅?因为共享,多个 Collector / Subscriber 可以共享同一个生产流程。这究其根本是因为 SharedFlow 是“热”的,它的数据生产和消费是独立的。
也正是因为生产和消费是独立的,数据的生产可能比数据收集要早,如果一个流的初始化工作非常耗时,也可以利用这一点特性,使用 SharedFlow 让生产流程提前启动。但是要注意,数据生产早于数据收集意味着可能会漏数据,因为 SharedFlow 不像普通 Flow 那样每次都从头完整地收集数据,它只关心收集过程中流产出的每条数据。
"事件订阅"需要的就是共享同一套生产流程,它也不需要从头开始收集数据,因此 SharedFlow 最典型的应用场景就是事件流。
当然,如果你有任何的场景,需要共享同一套生产流程、或者将数据生产和消费的流程拆分,能够接收"漏数据"的后果,那么都可以考虑使用 SharedFlow。