Kotlin之SharedFlow和Stateflow

SharedFlow

SharedFlow是一個hot stream. sharedflow有以下特點:

  1. 沒有默認值
  2. 可以保持舊值
  3. emit會掛起直到所有的訂閱者處理完成
public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
)

replay: 重新發送給新訂閱者之前值的個數

extraBufferCapacity:除了replay緩衝區個數之外的緩衝區的值。當有剩餘空間的時候emit就不會掛起

onBufferOverflow:當extraBufferCapacity溢出時的處理。有下面三種處理方式:

public enum class BufferOverflow {
    /**
     * Suspend on buffer overflow.
     */
    SUSPEND,

    /**
     * Drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend.
     */
    DROP_OLDEST,

    /**
     * Drop **the latest** value that is being added to the buffer right now on buffer overflow
     * (so that buffer contents stay the same), do not suspend.
     */
    DROP_LATEST
}

使用demo來測試這幾個參數的作用:

1.測試replay的作用

fun `sharedflow Reply = 1`()= runBlocking{
    var sharedflow = MutableSharedFlow<Int>(replay = 1)
    GlobalScope.launch {
        sharedflow.emit(111)
        sharedflow.emit(222)
    }
    val job1 = GlobalScope.launch {
        delay(5000)
        sharedflow.collect{
            println("first job : $it")
        }
    }

    val job2 = GlobalScope.launch {
        delay(10000)
        sharedflow.collect{
            println("second job : $it")
        }
    }

    job1.join()
    job2.join()
}

結果:

first job : 222
second job : 222

上面的例子可以看出,設置了replay爲1(默認爲0),job1和job2最後打印的都是“222”

2.測試extraBufferCapacity的作用

fun `sharedflow emit suspend`() = runBlocking{
    var sharedflow = MutableSharedFlow<Int>()

    val job1 = GlobalScope.launch {
        sharedflow.collect{
            //println("${gettime()}:first job: $it")
        }
    }
    val job2 = GlobalScope.launch {
        sharedflow.collect{
            delay(5000)
            //println("${gettime()}:second job: $it")
        }
    }
    val job3 = GlobalScope.launch {
        for (i in 1..5){
            delay(500)
            println("${gettime()}:start--------emit $i")
            sharedflow.emit(i)
            println("${gettime()}:end--------emit $i")
        }
    }
    job1.join()
    job2.join()
    job3.join()
}

結果:

第一個emit沒有掛起
11:20:46:699:start--------emit 1
11:20:46:704:end--------emit 1
第二個emit掛起了4s左右
11:20:47:206:start--------emit 2
11:20:51:711:end--------emit 2
第三個emit掛起了6s左右
11:20:52:217:start--------emit 3
11:20:56:716:end--------emit 3
第四個emit掛起了4s左右
11:20:57:219:start--------emit 4
11:21:1:717:end--------emit 4
第五個emit掛起了6s左右
11:21:2:218:start--------emit 5
11:21:6:720:end--------emit 5

代碼中job2在collect時候delay了5s,從emit的結果看,從emit 2開始從start emit到end emit中間大概執行了4s多的時間。extraBufferCapacity默認爲0

下面再測試一下,將extraBufferCapacity設置爲2:

var sharedflow = MutableSharedFlow<Int>(extraBufferCapacity = 2)

測試結果:

emit沒有掛起
11:24:47:843:start--------emit 1
11:24:47:848:end--------emit 1
emit沒有掛起
11:24:48:354:start--------emit 2
11:24:48:356:end--------emit 2
emit沒有掛起
11:24:48:860:start--------emit 3
11:24:48:861:end--------emit 3
emit掛起3s多
11:24:49:365:start--------emit 4
11:24:52:852:end--------emit 4
emit掛起6s多
11:24:53:355:start--------emit 5
11:24:57:857:end--------emit 5

從結果看emit1 到 emit3 的emit沒有耗費太多的時間,從emit4開始中間分別耗費了3s,6s。從demo的測試來看當緩衝區滿了之後執行了emit執行了掛起操作。

3.測試onBufferOverflow值對sharedflow的影響,修改onBufferOverflow = BufferOverflow.DROP_OLDEST,這個意思是丟棄最先的值

 fun `sharedflow emit suspend`() = runBlocking{
        var sharedflow = MutableSharedFlow<Int>(replay = 1,extraBufferCapacity = 0,onBufferOverflow = BufferOverflow.DROP_OLDEST)

        val job1 = GlobalScope.launch {
            sharedflow.collect{
                println("${gettime()}:first job: $it")
            }
        }
        val job2 = GlobalScope.launch {
            sharedflow.collect{
                delay(5000)
                println("${gettime()}:second job: $it")
            }
        }
        val job3 = GlobalScope.launch {
            for (i in 1..5){
                delay(500)
                //println("${gettime()}:start--------emit $i")
                sharedflow.emit(i)
               // println("${gettime()}:end--------emit $i")
            }
        }
        job1.join()
        job2.join()
        job3.join()
    }

運行結果:

1:20:26:627:first job: 1
1:20:27:129:first job: 2
1:20:27:637:first job: 3
1:20:28:140:first job: 4
1:20:28:643:first job: 5
1:20:31:632:second job: 1
1:20:36:639:second job: 5

從上面運行結果看job1執行完成了,job2只輸出了第一個值和最後一個值。job2將2,3,4幾個值都丟失了。

onBufferOverflow != BufferOverflow.SUSPEND時,replay和extraBufferCapacity不能同時爲0(默認值),否則運行時會出現如下錯誤:

replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy DROP_OLDEST
java.lang.IllegalArgumentException: replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy DROP_OLDEST
	at kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow(SharedFlow.kt:246)
	at com.example.flowdemo.ExampleUnitTest$sharedflow emit suspend$1.invokeSuspend(ExampleUnitTest.kt:165)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at com.example.flowdemo.ExampleUnitTest.sharedflow emit suspend(ExampleUnitTest.kt:164)

將onBufferOverflow 的值改爲DROP_LATEST

var sharedflow = MutableSharedFlow<Int>(replay = 1,extraBufferCapacity = 0,onBufferOverflow = BufferOverflow.DROP_LATEST)

運行結果:

1:36:44:978:first job: 1
1:36:45:481:first job: 2
1:36:49:983:second job: 1
1:36:54:989:second job: 2

從結果看只輸出去了前兩個值,後面的值全部丟掉了。

4.sharedflow是不防抖的,即如果連續放入相同的值,那麼每個值collect都會響應一次。

fun `sharedflow shake`()= runBlocking{
    val sharedflow = MutableSharedFlow<String>()
    val job1 = GlobalScope.launch {
        sharedflow.collect {
            println("job---$it")
        }
    }
    val job2 = GlobalScope.launch {
        for (i in 1..5){
            delay(100)
            sharedflow.emit("hello laworks")
        }
    }
    job1.join()
    job2.join()
}

運行結果

job---hello laworks
job---hello laworks
job---hello laworks
job---hello laworks
job---hello laworks

這個和stateflow做一個對比

@Test
fun `stateflow shake`()= runBlocking{
    val stateflow = MutableStateFlow("state init")
    val job1 = GlobalScope.launch {
        stateflow.collect {
            println("job---$it")
        }
    }
    val job2 = GlobalScope.launch {
        for (i in 1..5){
            delay(100)
            stateflow.emit("I'am stateflow")
        }
    }
    job1.join()
    job2.join()
}

運行結果:

job---state init
job---I'am stateflow

從上面對比的結果可以看出stateflow是防抖的。

stateFlow

sharedflow是不防抖的,但是stateflow是防抖的,從上面的例子可以看出,下面是源碼的分析:

源碼如下:

synchronized(this) {
            val oldState = _state.value
            if (expectedState != null && oldState != expectedState) return false // CAS support
            
            /*********return if equal*********/
            if (oldState == newState) return true // Don't do anything if value is not changing, but CAS -> true
            
            _state.value = newState
            curSequence = sequence
            if (curSequence and 1 == 0) { // even sequence means quiescent state flow (no ongoing update)
                curSequence++ // make it odd
                sequence = curSequence
            } else {
                // update is already in process, notify it, and return
                sequence = curSequence + 2 // change sequence to notify, keep it odd
                return true // updated
            }
            curSlots = slots // read current reference to collectors under lock
        }

從源碼上可以看出來,當值沒有變化的時候就直接return了,不會做任何的處理。這也就是爲什麼stateflow是防抖的。

下面看看關於stateflow值丟失的問題:

@Test
fun `stateflow suspend`() = runBlocking {
    val stateflow = MutableStateFlow(0)
    val job1 = GlobalScope.launch {
        stateflow.collect{
            println("job1----$it")
        }
    }
    val job2 = GlobalScope.launch {
        stateflow.collect{
            delay(5000)
            println("job2----$it")
        }
    }
    val job3 = GlobalScope.launch {
        for(i in 1..5){
            //delay(500)
            println("${gettime()}:start--------emit $i")
            stateflow.emit(i)
            println("${gettime()}:end--------emit $i")
        }
    }
    job1.join()
    job2.join()
    job3.join()
}

輸入的結果如下:

10:3:3:681:start--------emit 1
job1----0
10:3:3:684:end--------emit 1
job1----1
10:3:3:684:start--------emit 2
10:3:3:684:end--------emit 2
10:3:3:684:start--------emit 3
10:3:3:684:end--------emit 3
10:3:3:684:start--------emit 4
10:3:3:684:end--------emit 4
10:3:3:684:start--------emit 5
10:3:3:685:end--------emit 5
job1----5
job2----0
job2----5

結果看job1分別輸出了0,1,5. job2分別輸出了0和5. 並且emit很快就完成了,這是爲什麼?爲什麼job1沒有輸出所有的值,job2的延遲也沒有像sharedflow一樣將emit掛起?

首先說stateflow沒有buffer的概念,它只能存一個值,所以在有值的時候就會去更新。再看看collect的實現:

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方法會繼續調用帶參數的collect方法,參數裏面的emit方法就是我們外面的實現。

在看看帶參數的collect在stateflow裏面的實現:

override suspend fun collect(collector: FlowCollector<T>) {
    val slot = allocateSlot()
    try {
        if (collector is SubscribedFlowCollector) collector.onSubscription()
        val collectorJob = currentCoroutineContext()[Job]
        var oldState: Any? = null // previously emitted T!! | NULL (null -- nothing emitted yet)
        // The loop is arranged so that it starts delivering current value without waiting first
        while (true) {
           ....
            if (oldState == null || oldState != newState) {
            	 //emit is the user action
                collector.emit(NULL.unbox(newState))
                oldState = newState
            }
            ....
        }
    } finally {
        freeSlot(slot)
    }
}

從代碼中可以看出:

1.這個方法裏面是一個無限循環

2.這個裏面在不斷的取值,並和之前的值做對比,如果一樣的話那麼就不會再調用emit方法了。

在這個方法裏面假設collector.emit(NULL.unbox(newState)) 執行的時間很長,那麼就有可能出現stateflow的值變化之後沒有及時取的情況。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章