概述
What
参考官方介绍StateFlow 和 SharedFlow | Android 开发者 | Android Developers
这两个类都是基于底层的Flow异步流 · Kotlin 官方文档 中文版 (kotlincn.net)
Why
当Kotlin Coroutines异步执行的上下文中需要状态管理,StateFlow和SharedFlow便是被设计用于这种场景, 进一步可以通过这样的状态管理去实现类似LiveData
的功能
How
参考StateFlow 和 SharedFlow | Android 开发者 | Android Developers 中的使用
本文针对SharedFlow的阐述其基本架构,然后通过详细的源码分析对基本架构进行详细的解释。由于StateFlow可以看作特殊SharedFlow,此篇暂且略过。
基本架构
上描述了SharedFlow的一个简单设计模型,看着非常简单,其实需要考虑几个问题
首先collect(订阅)这一步需要考虑如下几个问题
- 多线程订阅需要考虑到线程安全,这是一个基本前提,后续问题都需要考虑线程安全问题
- Slots如何动态管理多个Collector,以分配空间(add)解释可能遇到的问题,如下图
暂时无法在文档外展示此内容
- 如何保证订阅者生命周期,即只要没有取消订阅,发布者发布消息的时候,订阅者总能接收到
- 该模型类似生产者消费者模型,肯定会遇到生产者消费者速度不一致问题,如何解决?
其次再来看看emit(发布)需要考虑的问题
- 线程安全问题
- 线程安全的找到所有订阅者,进行
emit
- 生产者和消费者模型中的所有问题
除了上述所提出的需要解决的问题,SharedFlow还提供了不少feature, 如下
- 生产者消费者模型中由于生产者和消费者的速度不一致的三种模式,取最新,取最旧,等待值
- 事件重放,即新的订阅者开启时会收到之前的发送的数据,重放次数在代码里用
replay
表示 - 提供
LiveData
转换 - 冷热流的转换
大体框架定了,我们再以源码的角度详细阐述一下SharedFlow的创建,订阅和发布的过程。
源码分析
SharedFlow的创建过程
MutableSharedFlow
kotlin
public fun <T> MutableSharedFlow(
// 重放
replay: Int = 0,
// 生产者和消费者模型中的缓存队列大小
extraBufferCapacity: Int = 0,
// 生产者和消费者模型中的舍弃方式,取最新,取最旧,等待值
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {
require(replay >= 0) { "replay cannot be negative, but was $replay" }
require(extraBufferCapacity >= 0) { "extraBufferCapacity cannot be negative, but was $extraBufferCapacity" }
require(replay > 0 || extraBufferCapacity > 0 || onBufferOverflow == BufferOverflow.SUSPEND) {
"replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy $onBufferOverflow"
}
// 前面其实已经判断了replay和extraBufferCapacity必须大于0,这里有点多余了
val bufferCapacity0 = replay + extraBufferCapacity
val bufferCapacity = if (bufferCapacity0 < 0) Int.MAX_VALUE else bufferCapacity0 // coerce to MAX_VALUE on overflow
return SharedFlowImpl(replay, bufferCapacity, onBufferOverflow)
}
- 常用的情况replay = 0, buffercapacity = 0
SharedFlowImpl
kotlin
private class SharedFlowImpl<T>(
private val replay: Int,
private val bufferCapacity: Int,
private val onBufferOverflow: BufferOverflow
) : AbstractSharedFlow<SharedFlowSlot>(), MutableSharedFlow<T>, CancellableFlow<T>, FusibleFlow<T> {
}
internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : SynchronizedObject() {
protected var slots: Array<S?>? = null // allocated when needed
private set
protected var nCollectors = 0 // number of allocated (!free) slots
private set
private var nextIndex = 0 // oracle for the next free slot index
private var _subscriptionCount: MutableStateFlow<Int>? = null // init on first need
val subscriptionCount: StateFlow<Int>
get() = synchronized(this) {
// allocate under lock in sync with nCollectors variable
_subscriptionCount ?: MutableStateFlow(nCollectors).also {
_subscriptionCount = it
}
}
}
可以看其继承了AbstractSharedFlow
,其中有两个重要变量后面会提到,slots
和
nCollectors
SharedFlow的订阅过程
订阅过程即把订阅者注册到发布者(AbstractSharedFlow
)中
Collect.kt
kotlin
public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit =
collect(object : FlowCollector<T> {
override suspend fun emit(value: T) = action(value)
})
flow调用了collect就会新建一个collector
,这里可以理解为订阅者,我们可以想到这个collector在后续的步骤肯定是会注册到发布者中。
该方法继续调用SharedFlow中的collect函数
SharedFlow -> collect
kotlin
@Suppress("UNCHECKED_CAST")
override suspend fun collect(collector: FlowCollector<T>) {
// 一个collector对应着一个slot
val slot = allocateSlot()
try {
if (collector is SubscribedFlowCollector) collector.onSubscription()
val collectorJob = currentCoroutineContext()[Job]
// 第一层while用于collect不断接收值
while (true) {
var newValue: Any?
// 第二层while用于等待上游发送值
while (true) {
// 两件事
// 1. 尝试直接获得上游发送的值
// 2. slot如果处于unlocked状态,则resume上游保存的续体
newValue = tryTakeValue(slot) // attempt no-suspend fast path first
// 拿到了就退出
if (newValue !== NO_VALUE) break
// 没拿到继续等待值, 等待上游emit
awaitValue(slot) // await signal that the new value is available
}
collectorJob?.ensureActive()
// suspend 执行collect里的block {}
collector.emit(newValue as T)
}
} finally {
freeSlot(slot)
}
}
两个循环 + awaitValue
表示SharedFlow的collect
正常情况下是不会退出,整体逻辑如下
tryTakeValue
循环获取上游发送的值,有值时退出,执行collector.emit
,没值时交给awaitvalue
去挂起等待上游通过slot.cont
发送值。参考tryTakeValuecollector.emit
是一个suspend函数,当该suspend函数执行完成后,又会继续通过tryTakeValue
去取上游数据。awaitvalue
目的是给slot
赋值cont
即slot.cont = awaitvalue.cont
AbstractSharedFlow -> allocateSlot
kotlin
protected fun allocateSlot(): S {
// Actually create slot under lock
var subscriptionCount: MutableStateFlow<Int>? = null
val slot = synchronized(this) {
val slots = when (val curSlots = slots) {
null -> createSlotArray(2).also { slots = it }
else -> if (nCollectors >= curSlots.size) {
// 只要订阅者大于slots的大小,说明空间不够需要分配
curSlots.copyOf(2 * curSlots.size).also { slots = it }
} else {
curSlots
}
}
var index = nextIndex
var slot: S
while (true) {
slot = slots[index] ?: createSlot().also { slots[index] = it }
index++
if (index >= slots.size) index = 0
// new size 2,但是只分配1个内存
if ((slot as AbstractSharedFlowSlot<Any>).allocateLocked(this)) break // break when found and allocated free slot
}
nextIndex = index
nCollectors++
subscriptionCount = _subscriptionCount // retrieve under lock if initialized
slot
}
// increments subscription count
subscriptionCount?.increment(1)
return slot
}
这一部分就是分配slot,可以简单理解为slot
为保存订阅者的空间。Slot的具体数据结构可参考附录[Slot的数据结构]
SharedFlow -> tryTakeValue
kotlin
private fun tryTakeValue(slot: SharedFlowSlot): Any? {
var resumes: Array<Continuation<Unit>?> = EMPTY_RESUMES
val value = synchronized(this) {
val index = tryPeekLocked(slot)
// index < 0 代表slot locked
if (index < 0) {
NO_VALUE
} else {
// slot unlocked, resume上游
val oldIndex = slot.index
val newValue = getPeekedValueLockedAt(index)
slot.index = index + 1 // points to the next index after peeked one
// 得到上游可以resume的cont
resumes = updateCollectorIndexLocked(oldIndex)
newValue
}
}
// resume上游cont
for (resume in resumes) resume?.resume(Unit)
return value
}
updateCollectorIndexLocked
有点复杂,其干了两件事
- 从slot和buffer里面找续体。先找buffer在找slot,将buffer和slot找到的所有resume添加到resumes中。buffer的续体为emit的suspend 可以简单理解为生产者, slot的续体为collect的suspend,可以简单理解为消费者。
这个地方举个例子:
生产者速度很快,消费者很慢,这样就会产生产者续体Emitter,作用是,等待消费者释放,调用resume,交给消费者。比如连续emit 1~10,消费者collector 10s中接受一个, buffer为3。
- 更新buffer及各种记录buffer数据的全局变量,将已经发送的buffer给clear掉
SharedFlow -> awaitValue
kotlin
private suspend fun awaitValue(slot: SharedFlowSlot): Unit = suspendCancellableCoroutine { cont ->
synchronized(this) lock@ {
//
val index = tryPeekLocked(slot) // recheck under this lock
if (index < 0) {
// 给slot分配续体,说明SharedFlowSlot的slot的cont是var
// 此处的cont就是在SharedFlow -> findSlotsToResumeLocked中slot.cont
slot.cont = cont // Ok -- suspending
} else {
//
cont.resume(Unit) // has value, no need to suspend
return@lock
}
slot.cont = cont // suspend, waiting
}
}
SharedFlow的emit过程
emit过程可以理解为发布/订阅中的发布过程
SharedFlow -> emit
kotlin
override suspend fun emit(value: T) {
if (tryEmit(value)) return // fast-path
// 该函数在满buffer且suspend的情况下执行
emitSuspend(value)
}
SharedFlow -> TryEmit
kotlin
override fun tryEmit(value: T): Boolean {
// 多少个收集者nCollectors就有多少个resumes
var resumes: Array<Continuation<Unit>?> = EMPTY_RESUMES
// 多线程安全,shareFlow可以在不同线程emit
val emitted = synchronized(this) {
if (tryEmitLocked(value)) {
// 找到collector对应的续体进行回调
resumes = findSlotsToResumeLocked(resumes)
true
} else {
false
}
}
// 这里是collect的cont,这里执行后collect中的代码就运行了
for (cont in resumes) cont?.resume(Unit)
return emitted
}
SharedFlow -> tryEmitLocked
kotlin
private fun tryEmitLocked(value: T): Boolean {
// Fast path without collectors -> no buffering
// 没有收集者,返回
if (nCollectors == 0) return tryEmitNoCollectorsLocked(value) // always returns true
// With collectors we'll have to buffer
// cannot emit now if buffer is full & blocked by slow collectors
// 当缓存容量超过最大容量时
if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) {
when (onBufferOverflow) {
// 如果是等待模式,返回使用emit
BufferOverflow.SUSPEND -> return false // will suspend
// 舍弃最新值即取最旧值, 所以选择不发射新值
BufferOverflow.DROP_LATEST -> return true // just drop incoming
// 舍弃旧值取最新值,所以选择发射新值
BufferOverflow.DROP_OLDEST -> {} // force enqueue & drop oldest instead
}
}
// 还有buffer就直接入队了
enqueueLocked(value)
bufferSize++ // value was added to buffer
// drop oldest from the buffer if it became more than bufferCapacity
// 移除最旧值,如果超过缓存max
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
}
从这个函数可以看出,emitSuspend
函数只在BufferOverflow.``SUSPEND
模式下才可能会调用,下面分析下该发射函数,在不是满Buffer的情况下,会通过findSlotsToResumeLocked
去找slot
SharedFlow -> findSlotsToResumeLocked
kotlin
private fun findSlotsToResumeLocked(resumesIn: Array<Continuation<Unit>?>): Array<Continuation<Unit>?> {
var resumes: Array<Continuation<Unit>?> = resumesIn
var resumeCount = resumesIn.size
forEachSlotLocked loop@ { slot ->
val cont = slot.cont ?: return@loop // only waiting slots
if (tryPeekLocked(slot) < 0) return@loop // only slots that can peek a value
if (resumeCount >= resumes.size) resumes = resumes.copyOf(maxOf(2, 2 * resumes.size))
resumes[resumeCount++] = cont
slot.cont = null // not waiting anymore
}
return resumes
}
该函数目的是找到slots中所有的数组
SharedFlow -> emitSuspend
kotlin
// 整体来讲类似滑动窗口逻辑
private suspend fun emitSuspend(value: T) = suspendCancellableCoroutine<Unit> sc@ { cont ->
// 最初没有续体, 如果buffer不够了,才会去slot里面找续体
var resumes: Array<Continuation<Unit>?> = EMPTY_RESUMES
val emitter = synchronized(this) lock@ {
// recheck buffer under lock again (make sure it is really full)
// double check
if (tryEmitLocked(value)) {
// 只有BufferOverflow.SUSPEND的情况才走这个分支
cont.resume(Unit)
resumes = findSlotsToResumeLocked(resumes)
return@lock null
}
// add suspended emitter to the buffer
// BufferOverflow.SUSPEND需要入队buffer
Emitter(this, head + totalSize, value, cont).also {
enqueueLocked(it)
queueSize++ // added to queue of waiting emitters
// synchronous shared flow might rendezvous with waiting emitter
// slot里面找续体
if (bufferCapacity == 0) resumes = findSlotsToResumeLocked(resumes)
}
}
// outside of the lock: register dispose on cancellation
emitter?.let { cont.disposeOnCancellation(it) }
// outside of the lock: resume slots if needed
// 这里的cont和该suspend的cont不太一样,其是之前Emitter保存的的cont
for (cont in resumes) cont?.resume(Unit)
}
根据collect
和现在emitSuspend
可以总结出当指定为BufferOverFlow.Suspend
时,处理生产者消费者的整体逻辑类似滑动窗口(想想计算机网络里学习的滑动窗口算法就是用来解决生产者消费者问题的方案之一),假设设置了extraBufferCapacity = 1
总共就有2个buffer。所以emit 1, 2时,由于订阅者消费速度较慢,需要保存在buffer中,不用resume()。当collector delay完成后,flow的slot立马分配了,于是再emit3的时候,能够找到slot的cont, 发送给了collector。
附录
SharedFlowSlot的数据结构
kotlin
private class SharedFlowSlot : AbstractSharedFlowSlot<SharedFlowImpl<*>>() {
@JvmField
var index = -1L // current "to-be-emitted" index, -1 means the slot is free now
@JvmField
var cont: Continuation<Unit>? = null // collector waiting for new value
override fun allocateLocked(flow: SharedFlowImpl<*>): Boolean {
if (index >= 0) return false // not free
index = flow.updateNewCollectorIndexLocked()
return true
}
override fun freeLocked(flow: SharedFlowImpl<*>): Array<Continuation<Unit>?> {
assert { index >= 0 }
val oldIndex = index
index = -1L
cont = null // cleanup continuation reference
return flow.updateCollectorIndexLocked(oldIndex)
}
}
- 此处的
cont
是协程中的续体,如何熟悉协程原理的话,这个续体里面其实持有Collector引用,用于异步回调
Q&A
整体来讲,SharedFlow是如何解决生产者与消费者问题的?
答:上游一个续体,下游多个续体;上游resume下游续体,下游resume上游续体;Buffer存储上游续体,slot存储下游续体
在collect()后能否执行代码
答:collect是一个suspend函数,所以需要在collect函数执行完成后后续代码才能执行,然后collect在协程正常时是一个无限while循环。所以后续代码是无法执行的。除非协程取消
slots为什么要设计成数组结构
答:这个问题可能理解得不全。代码内部使用了很多index下标访问和指定数量扩容机制,需要使用数组提高遍历性能和扩容灵活性。
总结与收获
StateFlow和SharedFlow可以平替LiveData
两者都是订阅和发布模型,然而LiveData由于粘性特点,有[LiveData数据倒灌]的问题。这个问题可以用SharedFlow解决(设置replay = 0),而其他需要该粘性特点的数据状态可以用StateFlow
解决
SharedFlow可以解决生产者消费者问题(背压)
由于SharedFlow
的后两个参数,使它有了数据流的处理能力,利用滑动窗口的思想解决了生产者和消费者问题。所以当遇到生产者和消费者在不同线程中且生产速率和消费速率不同时,可以用SharedFlow
解决。