深入理解StateFlow与ShareFlow

概述

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 发送值。参考tryTakeValue
  • collector.emit是一个suspend函数,当该suspend函数执行完成后,又会继续通过tryTakeValue去取上游数据。
  • awaitvalue目的是给slot赋值contslot.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解决。

相关推荐
大耳猫4 小时前
主动测量View的宽高
android·ui
帅次6 小时前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
枯骨成佛7 小时前
Android中Crash Debug技巧
android
kim565912 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼12 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ12 小时前
Android Studio使用c++编写
android·c++
csucoderlee13 小时前
Android Studio的新界面New UI,怎么切换回老界面
android·ui·android studio
kim565913 小时前
各版本android studio下载地址
android·ide·android studio
饮啦冰美式13 小时前
Android Studio 将项目打包成apk文件
android·ide·android studio
夜色。13 小时前
Unity6 + Android Studio 开发环境搭建【备忘】
android·unity·android studio