前言
與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_OLDEST
與DROP_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,就可以避免消費者拖累生產者速度的問題。