Kotlin SharedFlow 使用

前言

與Flow(冷流)不同,SharedFlow是熱流。它可以在多個消費者之間共享數據,并且可以在任何時候發射新值。這使得它非常適合用于多個消費者需要訪問相同數據的情況。不過本文并不打算深入講解SharedFlow原理,而是從結合demo從使用上來帶大家熟悉其特性。

SharedFlow 使用

最基礎的生產消費模型

 runBlocking {
        val sharedFlow = MutableSharedFlow<Int>()
        launch {
//消費者接收數據
            sharedFlow.collect {
                println("collect: $it")
            }
        }
        delay(100) //確保已經訂閱
      //生產者發射數據
        sharedFlow.emit(1)
    }

輸出:collect: 1

這種最簡單的模式下,是看到了預期的打印。這種應該是大家都能理解的生產者-消費者模型。

消費者沒有在單獨的協程

 runBlocking {
        val sharedFlow = MutableSharedFlow<Int>()

        sharedFlow.collect {
            println("collect: $it")
        }

        println("wait emit")

        delay(100) //確保已經訂閱
        sharedFlow.emit(1)
    }

猜一下結果?
沒有打印輸出

注意:這里區別是collect沒有在單獨的協程調用。因為collect是個掛起函數,會讓當前協程掛起。由于生產者還沒生產數據,消費者調用collect時發現沒數據后便掛起協程。所以生產者和消費者要處在不同的協程里。

生產者先發射,消費者再接收

 runBlocking {
        val sharedFlow = MutableSharedFlow<Int>()
        sharedFlow.emit(1)

        launch {
            sharedFlow.collect {
                println("collect: $it")
            }
        }
    }

結果:collect沒有收到數據。
原因:先發射了數據,此時消費者還沒有訂閱,導致數據丟失。這也就說明了SharedFlow默認是沒有粘性的。

關于”粘性“:對于新的訂閱者重放其已發出的值。這意味著當一個新的訂閱者被添加到一個流中時,它將接收到流先前發出的所有值,即使它們在訂閱之前已經被發出。

我們大膽猜想下,要讓SharedFlow具備粘性,就應該讓其具有緩存機制。

歷史數據的重放機制
用過livedata的人都只知道,即使先更新了數據,但每次添加了新的觀察者,都能收到最新的數據,但SharedFlow默認是不具備這種能力的。但是這并不代表SharedFlow不行,而是需要一定的配置才能實現這種能力,其實SharedFlow這方面能力比livedata更強大,LiveData只能收到一個最新的值,但是SharedFlow經過配置之后是可以收到多個發射的歷史數據。

public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {

先來看下MutableSharedFlow的構造方法中的參數

  • replay:重放次數。可以給訂閱者發送之前已經發射的數據,而發射數據的個數就是通過replay指定的;
  • extraBufferCapacity:是指Buffer中除了replay外,額外增加的緩存數量;
  • onBufferOverflow:緩存區滿了之后的溢出策略,有3種策略可供選擇。默認BufferOverflow.SUSPEND,緩存溢出時掛起;另外還有2種丟棄策略,DROP_OLDESTDROP_LATEST,分別是溢出時丟棄緩沖區中最舊的值和最新的值。

只配置replay

 runBlocking {
        val sharedFlow = MutableSharedFlow<Int>(replay = 1)
        sharedFlow.emit(1)

        launch {
            sharedFlow.collect {
                println("collect: $it")
            }
        }
    }

結果:collect是收到了數據

這是replay緩存區緩存數量為1,所以后面添加的收集者可以收到歷史數據。當然這個數量,你可以任意指定。

設置extraBufferCapacity

  runBlocking {
       val sharedFlow = MutableSharedFlow<Int>(
            replay = 2,
            extraBufferCapacity = 1
        )

        sharedFlow.emit(1)
        sharedFlow.emit(2)
        sharedFlow.emit(3)


        launch {
            sharedFlow.collect {
                println("collect: $it")
                delay(1000)
            }
        }

    }

先猜下這段代碼的結果是?

結果:collect: 2 collect: 3

可能很多人會很奇怪,前面說過緩存數量bufferSize是replay + extraBufferCapacity。那這段代碼中bufferSize是3啊,為什么collect只有2條數據呢?我敢說,這個問題很多用ShareFlow的人都沒有搞清楚(至少我在網上看到的博客是這樣的)。

我先不解釋,我們再來看一個例子

 runBlocking {
        val sharedFlow = MutableSharedFlow<Int>(
            replay = 2,
            extraBufferCapacity = 1
        )
        sharedFlow.emit(1)
        sharedFlow.emit(2)
        launch {
            sharedFlow.collect {
                println("collect: $it")
                delay(1000) //模擬處理背壓
            }
        }
        delay(200)
        sharedFlow.emit(3)
        sharedFlow.emit(4)
    }

結果:collect能收到4個數據

在這段代碼中,replay和extraBufferCapacity沒有變化。區別是先發射了2條數據1,2;然后開始訂閱,等訂閱了之后再發射數據3,4。而前一段代碼是先發射完所有數據在開始訂閱。

解析:extraBufferCapacity 是用于控制額外緩沖區的容量。額外緩沖區是一個用于存儲新值的緩沖區,當重放緩沖區已滿時,新值將被存儲在額外緩沖區中,直到有收集器準備好收集它為止。總結下,extraBufferCapacity對應的額外緩沖區是要在有收集者訂閱之后才能起作用,否則只有replay重放緩存區起作用。

emit也是個掛起函數

 runBlocking {
        val sharedFlow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 0)

        launch {
            sharedFlow.collect {
                println("collect: $it")
                delay(1000)
            }
        }

        launch {
            sharedFlow.collect {
                println("collect2: $it")
                delay(2000)
            }
        }

        delay(200)
        for (value in 1 until 4){
            println("emit value: $value")
            sharedFlow.emit(value)
        }

    }

輸出:
emit value: 1
collect: 1
collect2: 1
emit value: 2
collect: 2
collect2: 2
emit value: 3
collect: 3
collect2: 3

從打印可以看出:生產者要等待消費者消費完數據才進行下一次emit

通過replay或者extraBufferCapacity解決背壓問題
SharedFlow 是一種具有背壓支持的流。背壓是一種流量控制機制,用于控制數據流的速率,以確保接收端能夠處理數據的速度不超過其處理能力。在 SharedFlow 中,背壓機制通過以下方式實現:

當緩沖區已滿時, emit 函數將會被掛起,直到緩沖區中有足夠的空間來接收新的值。這樣可以避免生產者發送大量的數據,而消費者無法及時處理的情況,從而導致內存溢出或應用程序崩潰。

當消費者收到新值時,如果其處理速度較慢,那么生產者將會被掛起,直到消費者處理完所有的值,并釋放了足夠的空間來接收新的值。這樣可以避免消費者被壓垮,從而導致應用程序變得不可用。

因此, SharedFlow 的背壓機制可以確保生產者和消費者之間的數據流量得到平衡,以避免出現數據丟失或內存泄漏等問題。這使得 SharedFlow 成為處理大量數據的可靠和高效的方案之一。

但是這樣有個問題,生產者速度可能被消費者拖累。先來看一段代碼:

runBlocking {
//        val sharedFlow = MutableSharedFlow<Int>(replay = 3, extraBufferCapacity = 0)
        val sharedFlow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 3)

        launch {
            sharedFlow.collect {
                println("collect: $it")
                delay(1000)
            }
        }

        launch {
            sharedFlow.collect {
                println("collect2: $it")
                delay(2000)
            }
        }

        delay(200)
        for (value in 1 until 4){
            println("emit value: $value")
            sharedFlow.emit(value)
        }

輸出:emit value: 1
emit value: 2
emit value: 3
collect: 1
collect2: 1
collect: 2
collect2: 2
collect: 3
collect2: 3

這段代碼中消費者的速度明顯比生產者慢很多,但我們通過配置replay或者extraBufferCapacity來設置了緩存buffer,就可以避免消費者拖累生產者速度的問題。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容