我们知道 Flow.shareIn()
可以将一个现有的 Flow 转化为 SharedFlow,Flow 生产出来的每个数据会被 SharedFlow 广播 📢 给它的所有订阅者 。
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val dataFlow: Flow<Int> = flow {
var i = 0
while(true) {
delay(1000)
println("emit ${i++}")
emit(i)
}
}
val eventFlow: SharedFlow<Int> = dataFlow.shareIn(
scope = this,
started = SharingStarted.Eagerly,
)
// eventFlow.emit(-1) 🤷🏻🚫
}
由 Flow.shareIn()
创建出来的 SharedFlow 有一个局限性,它的数据源完全依赖上游 Flow 的数据发射,无法在 SharedFlow 的维度主动控制数据发射。这在需要动态产生事件或组合多数据源的场景下显得不够灵活,比如:
- 无法主动发射新事件:当需要手动触发某个事件时(如用户点击事件),
shareIn()
生成的 SharedFlow 只能被动转发原始 Flow 的数据; - 无法合并多数据源:如果事件需要来自不同模块(网络请求 | 本地数据库 | 用户操作),基于单一 Flow 的
shareIn()
难以实现多源头的事件聚合;
这时可以通过 MutableSharedFlow()
直接创建一个可自主控制的共享流。
MutableSharedFlow
调用 MutableSharedFlow()
创建一个 MutableSharedFlow 对象,后续可以调用这个对象的 emit()
方法从任意地方主动发出数据。
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val mutableSharedFlow: MutableSharedFlow<String> =
MutableSharedFlow<String>()
launch {
mutableSharedFlow.collect{
println(it)
}
}
delay(100)
launch {
mutableSharedFlow.emit("emit from Coroutine A")
}
launch {
mutableSharedFlow.emit("emit from Coroutine B")
}
}
// emit from Coroutine A
// emit from Coroutine B
点开源码发现 MutableSharedFlow 是 SharedFlow 的子接口,同时它也是 FlowCollector 的子接口:
// SharedFlow.kt
public interface MutableSharedFlow<T>
: SharedFlow<T>, FlowCollector<T> {
override suspend fun emit(value: T) // 📌
public fun tryEmit(value: T): Boolean
public val subscriptionCount: StateFlow<Int>
public fun resetReplayCache()
}
FlowCollector 的 emit()
让它拥有了发射数据的能力:
// FlowCollector.kt
public fun interface FlowCollector<in T> {
public suspend fun emit(value: T)
}
FlowCollector 相信大家都不陌生,flow { emit(1) }
发射数据时调用的 emit()
正是 FlowCollector 的函数:
另外,其实 Flow.shareIn()
的底层实现也是 MutableSharedFlow,只不过返回暴露给开发者的是只读的 SharedFlow 实例:
public fun <T> Flow<T>.shareIn(
...
): SharedFlow<T> {
val config = configureSharing(replay)
val shared = MutableSharedFlow<T>(
replay = replay,
extraBufferCapacity = config.extraBufferCapacity,
onBufferOverflow = config.onBufferOverflow
)
@Suppress("UNCHECKED_CAST")
val job = scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T)
return ReadonlySharedFlow(shared, job)
}
MutableSharedFlow()
VS Flow.shareIn()
如果你已经有一个冷 Flow,且希望在多个收集器之间共享数据,选择 Flow.shareIn()
;如果需要手动控制数据的发射或者创建一个事件总线,则选择 MutableSharedFlow()
。
MutableSharedFlow() 参数详解
public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>
replay: Int
第一个参数 replay 其实和 Flow.shareIn()
的参数 replay 是一样的,因为 Flow.shareIn()
的 replay 参数就是填到这里来的:
replay 的作用是利用 buffer 缓存最近发射过的数据,以在未来发送给新的订阅者(重播),这里不再赘述,详见上一篇文章。
extraBufferCapacity: Int
第二个参数 extraBufferCapacity,“额外的缓冲大小”
它的作用是增大 buffer 区域,extraBufferCapacity 默认为 0。设置 extraBufferCapacity > 0 有助于提升 Emitter 的吞吐量,因为 SharedFlow 的 buffer size = replay + extraBufferCapacity.
在上文中曾经提到过:
Flow.shareIn()
函数所创建出来的 SharedFlow,如果 replay <= 64,那么 buffer size = 64,如果 replay > 64,那么 buffer size = replay
在 MutableSharedFlow 这里又说 buffer size = replay + extraBufferCapacity,为什么呢?我们不妨看一下源码:
Flow.shareIn()
内部创建 MutableSharedFlow 的时候,replay 参数是原封不动填上去的,而 extraBufferCapacity 是用的 SharingConfig.extraBufferCapacity
,那我们就找创建 SharingConfig 的 configSharing()
方法:
configSharing()
方法接收了 replay 作为参数,在计算 extraBufferCapacity 的时候,从 replay 和 64 之间取较大值,再减去 replay。
因此,对于 SharedFlow 来说,buffer size 的大小确实等于 replay + extraBufferCapacity。
至于Flow.shareIn()
函数,如果 replay <= 64,那么 buffer size = 64,如果 replay > 64,那么 buffer size = replay,那是因为 Flow.shareIn()
内部自己做了处理。
话说回来,为什么增大 extraBufferCapacity 可以提升 Emitter 的吞吐量呢?
来看下面的代码,replay 为 0,extraBufferCapacity 也为 0,那么 buffer = replay + extraBufferCapacity = 0,处理每条数据要花费 1 秒,我们在发射数据时,记录生产每条数据花费的时间:
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val mutableSharedFlow: MutableSharedFlow<Int> =
MutableSharedFlow<Int>(
replay = 0, // 默认值
extraBufferCapacity = 0, // 默认值
onBufferOverflow = BufferOverflow.SUSPEND, // 默认值
)
launch {
mutableSharedFlow.collect {
delay(1000) // 消费每条数据花费 1 秒
}
}
val totalTime: Duration = measureTime {
(1..5).forEach {
val time: Duration = measureTime {
mutableSharedFlow.emit(it)
}
println("emit $it in $time")
}
}
println("emit all numbers in $totalTime")
}
生产第一条数据只花了几毫秒,后面的生产每条数据都花费了约 1 秒,总生产时间约为 4 秒:
因为 buffer size = 0,所以每一条数据的发送要等待上一条数据被处理完毕,不然生产出来也没地方存放,由此可见,下游的消费速度过慢,会导致上游生产发生阻塞:
现在我们把 extraBufferCapacity 设置为 2:
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val mutableSharedFlow: MutableSharedFlow<Int> =
MutableSharedFlow<Int>(
replay = 0, // 默认值
- extraBufferCapacity = 0, // 默认值
+ extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.SUSPEND, // 默认值
)
launch {
mutableSharedFlow.collect {
delay(1000) // 消费每条数据花费 300ms
}
}
val totalTime: Duration = measureTime {
(1..5).forEach {
val time: Duration = measureTime {
mutableSharedFlow.emit(it)
}
println("emit $it in $time")
}
}
println("emit all numbers in $totalTime")
}
通过对比可以发现,上游生产被阻塞的情况得到了一定改善,生产前面 3 条数据只花费了几微秒,阻塞从第 4 个数据才开始,总耗时也从 4 秒减少到 2 秒:
可见增大 extraBufferCapacity 确实能提升 Emitter 的吞吐量,防止在下游处理速度较慢时,上游的生产被阻塞。
当然,因为 buffer = replay + extraBufferCapacity,增大 replay 也能达到同样效果,但它的侧重点是“缓存重放”,如果你不想要重放,只想提升 Emitter 吞吐量,那么就应该使用 extraBufferCapacity,这也是为什么它命名为 extra BufferCapacity 而不是 BufferCapacity。
onBufferOverflow: BufferOverflow
// BufferOverflow.kt
public enum class BufferOverflow {
SUSPEND,
DROP_OLDEST,
DROP_LATEST
}
第三个参数,onBufferOverflow 是缓冲溢出策略,这个缓冲溢出策略跟 Flow.buffer()
操作符及 Channel 的缓冲溢出策略是一样的。MutableSharedFlow 的默认缓冲溢出策略是挂起 SUSPEND
,我们可以指定为 DROP_LATEST
或者 DROP_OLDEST
。
Flow.shareIn() 是不允许我们更改缓冲策略的,创建出的 SharedFlow 缓冲策略默认为挂起 SUSPEND。
当 Buffer 被填满时,emit()
会被挂起,这都是建立在 onBufferOverflow 为 SUSPEND 的前提下。除了 SUSPEND,还有两个数据丢弃策略:
- DROP_LATEST:丢弃 buffer 中最新的数据;
- DROP_OLDEST:丢弃 buffer 中最旧的数据;
要注意,当 BufferSize = 0 时,只支持 SUSPEND 挂起策略,指定丢弃策略是无效的。这很好理解,因为 Buffer 中没有数据,所以丢弃无从下手,丢弃策略的前提是 Buffer 的大小不为零,且数据被填满。
下面以实际例子看看两个缓冲溢出丢弃策略的行为:
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val mutableSharedFlow: MutableSharedFlow<Int> =
MutableSharedFlow<Int>(
replay = 1,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_LATEST,
)
launch {
mutableSharedFlow.collect {
delay(1000) // 消费每条数据花费 1 秒
println(it)
}
}
(1..5).forEach {
mutableSharedFlow.emit(it)
}
}
上图展示 DROP_LATEST 的效果。[replay = 1,extraBufferCapacity = 1]
- Emitter 发送 ❶ 进入 buffer,同时发给下游消费,buffer 保留 ❶是因为 replay = 1,需要缓存 1 个最新数据在未来重放;
- Emitter 发送 ❷ 进入 buffer 替换掉 ❶,因为 ❶ 已经被所有订阅者消费,而且也不是最新的一个数据,故而丢弃;
- Emitter 发送 ❸ 进入 buffer,现在 buffer 中的数据为 ❷❸。❷❸ 都还没有被消费,其中的 ❸ 将在未来被重放;
- Emitter 发送 ❹ 时,由于 buffer 已经满了,缓冲策略 DROP_LATEST 为丢弃最新数据,故 ❹ 被丢弃;
- Emitter 发送 ❺ 时,由于 buffer 已经满了,缓冲策略 DROP_LATEST 为丢弃最新数据,故 ❺ 被丢弃;
- Subscriber 处理完毕 ❶,开始处理 ❷,❷ 已经被现有的所有 Subscriber 消费了,而且它不是最新的一个数据,最新的是 ❸,因此将 ❷ 从 buffer 中移除;
- Subscriber 处理完毕 ❷,开始处理 ❸ ,❸ 已经被现有的所有 Subscriber 消费了,但它是最新的一个数据,不能将 ❸ 从 buffer 中移除,需要保留在未来重放。
再把缓冲策略改为 DROP_OLDEST:
suspend fun main(): Unit = coroutineScope { // this: CoroutineScope
val mutableSharedFlow: MutableSharedFlow<Int> =
MutableSharedFlow<Int>(
replay = 1,
extraBufferCapacity = 1,
- onBufferOverflow = BufferOverflow.DROP_LATEST,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
launch {
mutableSharedFlow.collect {
delay(1000) // 消费每条数据花费 1 秒
println(it)
}
}
(1..5).forEach {
mutableSharedFlow.emit(it)
}
}
上图展示 DROP_OLDEST 的效果。[replay = 1,extraBufferCapacity = 1]
- Emitter 发送 ❶ 进入 buffer,同时发给下游消费,buffer 保留 ❶是因为 replay = 1,需要缓存 1 个最新数据在未来重放;
- Emitter 发送 ❷ 进入 buffer 替换掉 ❶,因为 ❶ 已经被所有订阅者消费,而且也不是最新的一个数据,故而丢弃;
- Emitter 发送 ❸ 进入 buffer,现在 buffer 中的数据为 ❷❸。❷❸ 都还没有被消费,其中的 ❸ 将在未来被重放;
- Emitter 发送 ❹ 时,由于 buffer 已经满了,缓冲策略 DROP_OLDEST 为丢弃最旧数据,故 ❷ 被丢弃,此时 buffer 中的数据为 ❸❹;
- Emitter 发送 ❺ 时,由于 buffer 已经满了,缓冲策略 DROP_OLDEST 为丢弃最旧数据,故 ❸ 被丢弃,此时 buffer 中的数据为 ❹❺;
- Subscriber 处理完毕 ❶,开始处理 buffer 中最旧的一个数据 ❹,❹ 已经被现有的所有 Subscriber 消费了,而且它不是最新的一个数据,最新的是 ❺,因此将 ❹ 从 buffer 中移除;
- Subscriber 处理完毕 ❹,开始处理 ❺ ,❺ 已经被现有的所有 Subscriber 消费了,但它是最新的一个数据,不能将 ❺ 从 buffer 中移除,需要保留在未来重放。
onBufferOverflow 设为 SUSPEND 可以保证 Subscriber 一个不漏的消费掉所有从它开始收集之后生产的数据,但是可能会影响 Emitter 的速度,当设置为 DROP_XXX 时,可以保证 emit()
调用后立即返回,但是会导致 Subscriber 漏掉部分数据。
tryEmit()
如果不想让 emit()
被阻塞挂起,除了设置 DROP_XXX 之外,还有个方法就是调用 MutableSharedFlow.tryEmit()
,这是一个非 suspend 版本的 emit():
public interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> {
override suspend fun emit(value: T)
public fun tryEmit(value: T): Boolean
// ...
}
tryEmit()
返回一个 Boolean 值表示是否发射成功,返回 false 的前提是 onBufferOverflow 设置为 SUSPEND,且 Buffer 中空余位置为 0,此时 tryEmit()
的效果等同于 DROP_LATEST。如果使用缓冲溢出丢弃策略,tryEmit()
将总是返回 true。
asSharedFlow
MutableSharedFlow 提供了一个 asSharedFlow()
扩展函数,它可以将 MutableSharedFlow 转换为 SharedFlow。当你希望将 MutableSharedFlow 对外暴露供订阅使用,但又不希望外部有权限发送数据时,就可以利用该函数创建一个只读的 SharedFlow:
class MyViewModel: ViewModel() {
private val _event = MutableSharedFlow()
val event: SharedFlow = _event.asSharedFlow()
}