前言
對于SharedFlow使用可以看之前的文章 Kotlin SharedFlow 使用。在這篇文章中已經通過多個demo實戰幫大家總結了SharedFlow的一些特性和使用場景。但是也遺留了一些疑惑,所以本文打算通過輕度閱讀源碼的方式給大家答疑解惑。放心,不會從頭到尾的說。
SharedFlow源碼分析的重點
在源碼中可以分析的點很多,但我認為最重要的點有2點。emit和collect是如何關聯的,緩存機制是怎樣的。
從上一篇文章可以了解到emit和collect是掛起函數,但是否被掛起是有一定條件的。而且生產者,消費者出現的時機也會影響數據流的執行,這些問題就是本文要研究的重點。
下面分析emit和collect的小節中還會提出幾個問題,帶著大家通過解決問題的方式閱讀源碼。
回顧下之前文章的demo
為什么先發射數據,再collect就收不到數據呢
runBlocking {
val sharedFlow = MutableSharedFlow<Int>()
sharedFlow.emit(1)
launch {
sharedFlow.collect {
println("collect: $it")
}
}
}
為什么設置了replay緩存,超出其緩存數量的時候,會丟失前面的數據,只能收到最新replay數量的數據呢?比如下面demo,只能收到2和3
runBlocking {
//默認參數情況,先emit,再collect收不到數據
val sharedFlow = MutableSharedFlow<Int>(replay = 2, extraBufferCapacity = 0)
sharedFlow.emit(1)
sharedFlow.emit(2)
sharedFlow.emit(3)
launch {
sharedFlow.collect {
println("collect: $it")
}
}
}
emit發射數據
對于初學者來說,讀懂這個方法確實有一定難度,不過我換了一個角度來帶大家理解,從不同業務場景,使用角度來逐步分析,大致分為4種。
- 沒有收集者,無replay緩存
- 沒有收集者,有replay緩存(extraBufferCapacity緩存無影響)
- 有收集者,無replay緩存
- 有收集者,有replay緩存
override suspend fun emit(value: T) {
//嘗試發送數據,這是一個快速路徑,可以提高發送數據的效率。
if (tryEmit(value)) return // fast-path
//在掛起的協程中發送數據
emitSuspend(value)
}
emit方法先用不需要掛起的方式發數據,發失敗之后采用掛起的方式發。
以不掛起的方式發射數據
override fun tryEmit(value: T): Boolean {
var resumes: Array<Continuation<Unit>?> = EMPTY_RESUMES
val emitted = synchronized(this) {
//嘗試發射數據,發射成功返回true,失敗返回false
if (tryEmitLocked(value)) {
//找到需要恢復的協程,并將結果保存到 resumes 數組中,
resumes = findSlotsToResumeLocked(resumes)
true
} else {
false
}
}
//上面已經找到了需要恢復的協程,這里只需要恢復協程的執行
for (cont in resumes) cont?.resume(Unit)
return emitted
}
private fun tryEmitLocked(value: T): Boolean {
// 沒有收集者,一定返回true
if (nCollectors == 0) return tryEmitNoCollectorsLocked(value)
// 有收集者,緩存區已滿,超過replay + extraBufferCapacity數量,且消費者沒有消費最舊的數據(replayIndex)
if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) {
//執行下面的緩存溢出策略
when (onBufferOverflow) {
BufferOverflow.SUSPEND -> return false // will suspend
BufferOverflow.DROP_LATEST -> return true // just drop incoming
BufferOverflow.DROP_OLDEST -> {} //丟棄最舊的數據,這里暫不處理
}
}
//將數據加入到緩存數組中,這里不會掛起emit所在的協程
enqueueLocked(value)
bufferSize++ //緩存數組長度
// 上面的緩存溢出策略,丟棄最老數據是沒做處理的,實際上延遲在這里處理
if (bufferSize > bufferCapacity) dropOldestLocked()
// 如果replayCache中數據的數量超過了最大容量
if (replaySize > replay) {
// 更新replayIndex的值,replayIndex向前移動一位
updateBufferLocked(replayIndex + 1, minCollectorIndex, bufferEndIndex, queueEndIndex)
}
return true
}
這個方法已經涉及到shareflow緩存機制,所以有必要先來張圖大概了解下緩存機制。
shareflow緩存是個數組,大小由 bufferSize 控制,而緩存容量由 bufferCapacity 控制。緩存由3部分組成,replay緩存的數量,extraBufferCapacity緩存的數量,這2部分加起來就是buffered values,還有就是掛起時候的Emitter對象。圖中還展示了兩個慢速收集器的位置,即可能收集緩存隊列中的值的最慢速度的收集器的位置。這兩個位置分別由 minCollectorIndex 和 replayIndex 控制。
這個tryEmitLocked
方法很重要,因為它可以解釋之前文章中一些業務場景的困惑。 下面我們就來跟大家一起解剖下這個方法
tryEmitLocked方法沒有收集者
//走到這里,說明沒有收集者
private fun tryEmitNoCollectorsLocked(value: T): Boolean {
assert { nCollectors == 0 }
//replay緩存為0,就丟棄數據,emit方法就結束了
if (replay == 0) return true
enqueueLocked(value) // 加入到緩存數組
bufferSize++ // value was added to buffer
//若是emit發射的數量超過了重放個數,則丟棄最舊的值
if (bufferSize > replay) dropOldestLocked()
minCollectorIndex = head + bufferSize // a default value (max allowed)
return true
}
這段代碼也解釋了之前的2個疑惑
- 在不配置replay緩存的情況下,先emit發數據再collect是收不到數據的
- 在配置了replay的情況下,先emit再collect是能收到數據,但是 emit發射的數量超過了replay的話,就只能收到最新的replay個數的數據
如下2段代碼,運行結果都只能收到2和3
runBlocking {
//默認參數情況,先emit,再collect收不到數據
val sharedFlow = MutableSharedFlow<Int>(replay = 2, extraBufferCapacity = 0)
sharedFlow.emit(1)
sharedFlow.emit(2)
sharedFlow.emit(3)
launch {
sharedFlow.collect {
println("collect: $it")
}
}
}
runBlocking {
//這里配置了extraBufferCapacity根本不會起到效果
val sharedFlow = MutableSharedFlow<Int>(replay = 2, extraBufferCapacity = 2)
sharedFlow.emit(1)
sharedFlow.emit(2)
sharedFlow.emit(3)
launch {
sharedFlow.collect {
println("collect: $it")
}
}
}
從上面demo和
tryEmitNoCollectorsLocked
源碼分析可以看出:如果emit
發射到buffered values的數據數量超過了replay的值,會丟棄最舊的數據,保持buffered values中數據的數量最大為replay。
當有新的訂閱者時,會先從replayCache中獲取數據,在buffered values中,replayCache前的數據只對已經訂閱的訂閱者有用,而此時又沒有訂閱者,因此緩存超過replayCache最大容量的數據只會占用更多內存,是沒有意義的。記住:沒有收集者時,extraBufferCapacity是不會起作用的
tryEmitLocked方法有收集者
上面已經分析了這個方法沒有收集者的情況,接下來就分析下有收集者的情況
private fun tryEmitLocked(value: T): Boolean {
// 沒有收集者,一定返回true
if (nCollectors == 0) return tryEmitNoCollectorsLocked(value)
// 有收集者,緩存區已滿,超過replay + extraBufferCapacity數量,且消費者沒有消費最舊的數據(replayIndex)
if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) {
//執行下面的緩存溢出策略
when (onBufferOverflow) {
BufferOverflow.SUSPEND -> return false // will suspend
BufferOverflow.DROP_LATEST -> return true // just drop incoming
BufferOverflow.DROP_OLDEST -> {} //丟棄最舊的數據,這里暫不處理
}
}
//將數據加入到緩存數組中,這里不會掛起emit所在的協程
enqueueLocked(value)
bufferSize++ //緩存數組長度
// 上面的緩存溢出策略,丟棄最老數據是沒做處理的,實際上延遲在這里處理
if (bufferSize > bufferCapacity) dropOldestLocked()
// 如果replayCache中數據的數量超過了最大容量
if (replaySize > replay) {
// 更新replayIndex的值,replayIndex向前移動一位
updateBufferLocked(replayIndex + 1, minCollectorIndex, bufferEndIndex, queueEndIndex)
}
return true
}
有收集者,但沒有配置任何緩存
runBlocking {
//默認參數情況,先emit,再collect收不到數據
val sharedFlow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 0)
launch {
sharedFlow.collect {
println("collect: $it")
}
}
delay(200) //確保已經訂閱
sharedFlow.emit(1)
sharedFlow.emit(2)
sharedFlow.emit(3)
}
上面demo,是能收到1,2,3的。
從源碼可以看出,tryEmitLocked
方法中,有訂閱者的情況,即使沒有配置緩存也會執行enqueueLocked(value)方法把數據加入到緩存數組。
有訂閱者,且配置了replay或者extraBufferCapacity緩存,會多了一個緩存溢出策略。有3種策略,掛起協程,丟棄最老的數據,丟棄最新的數據。
emitSuspend掛起方式發數據
private suspend fun emitSuspend(value: T) = suspendCancellableCoroutine<Unit> sc@{ cont ->
var resumes: Array<Continuation<Unit>?> = EMPTY_RESUMES
val emitter = synchronized(this) lock@{
//再次檢查,確保緩存區滿了,因為滿了才會執行下面的邏輯
if (tryEmitLocked(value)) {
cont.resume(Unit)
resumes = findSlotsToResumeLocked(resumes)
return@lock null
}
// 創建Emitter,加入到buffer里
//可以去看下前面的那張圖,Emitter是加到緩存區的什么位置的
Emitter(this, head + totalSize, value, cont).also {
//加到之前的緩存數組
enqueueLocked(it)
queueSize++ // Emitter是掛起的,單獨記錄下數量
// 如果buffered value緩存沒有數據,則收集已經掛起的訂閱者的續體,保存到局部變量resumes中
if (bufferCapacity == 0) resumes = findSlotsToResumeLocked(resumes)
}
}
// outside of the lock: register dispose on cancellation
emitter?.let { cont.disposeOnCancellation(it) }
// 恢復掛起的訂閱者
for (r in resumes) r?.resume(Unit)
}
這個方法比較簡單,緩存區滿了,創建Emitter加到buffer緩存區。
小結
閱讀emit發射數據的流程,可以分2部分完成,不掛起發射和掛起發射。
如果沒有收集者,emit永遠不會掛起。
如果有收集者,并且buffered values緩存容量已滿并且最舊的數據沒有被消費,則emit有機會被掛起,當然這取決于你的溢出策略。
collect消費數據
override suspend fun collect(collector: FlowCollector<T>): Nothing {
//分配槽位
val slot = allocateSlot()
try {
if (collector is SubscribedFlowCollector) collector.onSubscription()
val collectorJob = currentCoroutineContext()[Job]
while (true) {
var newValue: Any?
while (true) {
//嘗試獲取值,獲取到了就跳出循環,獲取不到就掛起等待
newValue = tryTakeValue(slot) // attempt no-suspend fast path first
if (newValue !== NO_VALUE) break
awaitValue(slot) // await signal that the new value is available
}
//判斷訂閱者所在協程是否是存活
collectorJob?.ensureActive()
//回調到collect方法的lambda
collector.emit(newValue as T)
}
} finally {
freeSlot(slot)
}
}
關于slot的理解,可以看下圖
- 消費者開始collect,根據index找到buffer下標為0的元素即為可以消費的元素;
- 拿到0號數據后,slot.index=1,找到buffer下標為1的元素
index++,重復上訴步驟
private fun tryTakeValue(slot: SharedFlowSlot): Any? {
var resumes: Array<Continuation<Unit>?> = EMPTY_RESUMES
val value = synchronized(this) {
// 從slot中獲取index
// index表示當前應該從緩存數組的index位置中獲取數據
val index = tryPeekLocked(slot)
if (index < 0) {
//沒有數據,返回空數據的標識
NO_VALUE
} else {
val oldIndex = slot.index
//從緩存數組buffer中獲取index對應的數據
val newValue = getPeekedValueLockedAt(index)
//slot索引加1,表示獲取下個數據的位置
slot.index = index + 1 // points to the next index after peeked one
resumes = updateCollectorIndexLocked(oldIndex)
newValue
}
}
//恢復協程
for (resume in resumes) resume?.resume(Unit)
return value
}
tryPeekLocked
方法,是判斷數據所在的位置是否符合要求。
private suspend fun awaitValue(slot: SharedFlowSlot): Unit = suspendCancellableCoroutine { cont ->
synchronized(this) lock@{
//再次檢查index
val index = tryPeekLocked(slot) // recheck under this lock
if (index < 0) {
//保存續體cont到slot
slot.cont = cont // Ok -- suspending
} else {
//說明有值,不需要再繼續掛起了,通過resume恢復協程
cont.resume(Unit) // has value, no need to suspend
return@lock
}
slot.cont = cont // suspend, waiting
}
}
suspendCancellableCoroutine
是一個掛起函數,用于創建可取消的協程。在協程中調用suspendCancellableCoroutine
函數時,它會創建一個CancellableContinuation
對象,并將其傳遞給一個lambda表達式。該lambda表達式中的代碼可以使用suspend
關鍵字掛起當前協程,并在某個條件滿足時或協程被取消時恢復協程。
collect小結
collect方法會構造Slot對象,然后開啟死循環去不斷匹配緩存區的數據。具體是根據Slot 中的index匹配緩存區buffer中的數據,如果匹配到了,執行collect閉包;匹配不到就掛起協程,掛起的協程會在有新數據時被生產者所恢復。無論是否有生產者,只要沒拿到數據,collect都會被掛起。