一文看懂 Jetpack Compose 快照系統

1. 引言

Compose 通過名為“快照(Snapshot)”的系統支撐狀態管理與重組機制的運行。快照作為一個底層設施,在我們的日常開發中很少直接接觸,本文就為大家揭開快照的神秘面紗。我們在開頭先拋出幾個問題,希望在文章結束時大家能夠找到答案,對快照也就算有了初步了解了。

  • 快照能做什么?
  • 快照與狀態的關系?
  • 快照與線程的關系?
  • 快照與重組的關系?

注意:本文出現的源碼基于版本 1.2.0-alpha06。本文重在幫助大家建立認知,對源碼的介紹只是點到為止,請放松閱讀。

我們知道 Compose 庫從上到下分為多層:Material > UI > Runtime > Compiler 。快照系統位于 Runtime 層 androidx/compose/runtime/snapshots。 它自成體系,可以脫離 Compose UI 甚至 Compiler 單獨使用,只依賴 Runtime 即可使用快照功能,本文出現的示例代碼均可以不依賴 UI 運行。

implementation "androidx.compose.runtime:runtime:$compose_version"

2. 快照的基本操作

快照并非 Compose Runtime 的原創概念,它其實是一個 MVCC 系統的實現,MVCC 全稱 Multiversion Concurrency Control (多版本并發控制),常用于數據庫管理系統,實現事務并發,提升數據庫性能,其模型與 Git 分支管理系統也有點類似,因此我們可以類比數據庫的事務或者 Git 的分支來理解快照機制。

快照的創建

先看下面的例子:

fun test() {
    // 創建狀態(主線開發)
    val state = mutableStateOf(1)

    // 創建快照(開分支)
    val snapshot = Snapshot.takeSnapshot()

    // 修改狀態(主線修改狀態)
    state.value = 2

    println(state.value) // 打印1

    snapshot.enter {//進入快照(切換分支)
        // 讀取快照狀態(分支狀態)
        println(state.value) // 打印1 
    }

    // 讀取狀態(主線狀態)
    println(state.value) // 打印2

    // 廢棄快照(刪除分支)
    snapshot.dispose()
}

例子中展示了快照的基本功能:隔離訪問Snapshot.takeSnapshot() 創建了一個快照,通過調用其 enter() 進入此快照。在快照上只能看到快照被創建時刻的最新狀態,看不到此后的變化。

將快照類比成 Git 系統,程序默認處于 GlobalSnapshot 全局快照中,這相當于 Git 的 Main 分支。從全局快照上創建并進入子快照,就如同在 Main 上創建并切換分支,分支代碼保持分支創建時的狀態,看不到主線或其他分支的修改。當然 Git 的隔離對象是代碼,而快照的隔離對象是“狀態”,也就是 mutableStateOf 創建的一個 StateObject 實例。

使用下面這些方法都可以創建 StateObject 對象,它們都可以被快照隔離:

  • mutableStateOf/MutableState
  • mutableStateListOf/SnapshotStateList
  • mutableStateMapOf/SnapshotStateMap
  • derivedStateOf
  • rememberUpdatedState
  • collect*AsState

快照的修改 & 提交

上面的例子中 enter() 內只是讀取了快照狀態,如果我們試圖更新狀態則會拋出異常。takeSnapshot() 創建的是一個只讀快照,不允許對狀態有寫操作。如果需要更新狀態,需要使用 takeMutableSnapshot() 創建可寫的快照:

// 創建可寫的快照
val snapshot = Snapshot.takeMutableSnapshot()

snapshot.enter {
    // 對快照狀態進行變更
    state.value = 2

    println(state.value) // 打印2
}

// snaphot之外看不到對快照狀態的修改。
println(state.value) // 打印1

如上,我們對狀態的修改同樣會被快照隔離。快照中的狀態修改只對當前快照可見,在快照之外看不到,如果我們希望快照的修改通知到全局,可以使用 apply 提交這個修改。類比到 Git 就好似通過 merge 將分支合并回了主線。

snapshot.enter {
    // ...
}

// 提交snapshot中的狀態修改
snapshot.apply()

// 快照外可以看到snapshot中的修改
println(state.value) // 打印2

我們還可以使用 withMutableSnapshot 簡化代碼,它可以在“切換回主線”時自動提交變更

Snapshot.withMutableSnapshot {
    state.value = 2
}

println(state.value) // 打印2

注意:git merge 可以在任意分支之間進行合并,而快照的 apply 永遠是從當前快照提交到“父快照”。快照上允許嵌套創建快照,因此快照存在父子關系。

3. 訪問隔離的實現原理

前面介紹了快照的基本功能是對狀態的訪問隔離。Compose 狀態本質上是一個 StateObject 實例,為什么在不同快照下訪問同一個 StateObject 實例,卻能讀取到不同結果呢?研究源碼后會發現,與其說是快照隔離了狀態,倒不如說是狀態關聯了快照

狀態關聯快照

StateObject 內部維護了一個 StateRecord 鏈表。

所有快照在創建時都會被賦予一個全局遞增的 id,即 SnapshotId,StateObject 被寫入的狀態值會關聯當前快照的 snapshotId ,然后保存在 StateRecord 中。當我們在不同快照下訪問 StateObject 時,通過遍歷 SatateRecord 鏈表只能看到當前快照允許看到的值

可見,Compose 的 State 天生支持在快照中訪問,所以 Compose 的狀態也經常被稱為快照狀態( Snapshot State),快照狀態通過 snapshotId 實現“多版本并發控制”的目的。

管理 SnapshotId

那么“當前快照允許看到的值”是如何確定的呢?到這里大家應該很容易想到,其實就是比較訪問中的 StateRecord 與當前快照的 snapshotId 。當我們在快照上讀取 StateObject 時,會走到 Snapshot.kt 的 readable 中 :

//androidx/compose/runtime/snapshots/Snapshot.kt

//遍歷鏈表,根據 snapshotId 返回符合當前快照讀取條件的 StateRecord
private fun <T : StateRecord> readable(r: T, id: Int, invalid: SnapshotIdSet): T? {
    var current: StateRecord? = r
    var candidate: StateRecord? = null
    //while 循環中遍歷鏈表
    while (current != null) {
        //valid 方法檢查 StateRecord 是否符合條件
        if (valid(current, id, invalid)) {
            // 符合條件且 snapshotId 最大的 StateRecord 作為結果返回。
            candidate = if (candidate == null) current
            else if (candidate.snapshotId < current.snapshotId) current else candidate
        }
        current = current.next
    }
    if (candidate != null) {
        @Suppress("UNCHECKED_CAST")
        return candidate as T
    }
    return null
}

/**
  * 檢查 StateRecord 是否可以被讀取:
  * 1. StateRecord#snapshotId != INVALID_SNAPSHOT。
  * 2. StateRecord#snapshotId 不大于當前快照 id。
  * 3. StateRecord#snapshotId 不在 invalid 集合中
*/
private fun valid(currentSnapshot: Int, candidateSnapshot: Int, invalid: SnapshotIdSet): Boolean {
    return candidateSnapshot != INVALID_SNAPSHOT && candidateSnapshot <= currentSnapshot &&
        !invalid.get(candidateSnapshot)
}

代碼很清晰,如大家所料,這里通過 snapshotId 的比較來決定 StateRecord 是否可讀。因為快照被賦予了全局自增 id,理論上小于當前 snapshotId 的狀態值是快照創建前被寫入的,所以應該對當前快照可見。我們注意到除了 snapshotId 的比較之外,還要求 StateRecord#snapshotId 不能位于 invalid 集合中。

//androidx/compose/runtime/snapshots/Snapshot.kt

open class MutableSnapshot internal constructor(
    id: Int, // 快照id
    invalid: SnapshotIdSet, //快照黑名單
    override val readObserver: ((Any) -> Unit)?, // 讀回調,后文介紹
    override val writeObserver: ((Any) -> Unit)? // 寫回調,后文介紹
) : Snapshot(id, invalid) 

MutableSnapshot 的定義如上,其中 invalid 成員代表一個快照黑名單。處于黑名單中的 id,即使比當前快照 id 小,也視為不可見內容。我們前面介紹過快照的提交,在子快照未提交之前,即使它的 id 小于全局快照也不應該被全局看見,因此在正式提交前之前會被加入全局快照的這個黑名單。

創建/提交快照時的 id 變化如上圖所示:

  1. 我們在 GlobalSnapshot 中創建子快照,id 賦值為 2
  2. 為了讓子快照中訪問不到父快照后續的狀態變化,子快照創建后 GlobalSnapshot 的 id 升級至 3
  3. 為了讓 GlobalSnapshot 看不到子快照的狀態變化,將 2 加入 invalid
  4. 子快照提交后,GlobalSnapshot 的 invalid 中移除 2,子快照狀態全局可見。

上面過程中出現了 id 升級的概念,可見快照提交的本質就是通過升級父快照 id 讓子快照狀態全局可見。這與 git merge 之后移動分支的 head 位置也有著異曲同工之處。

4. 狀態讀寫感知

快照系統除了對狀態的讀寫進行隔離,還可以對狀態的讀寫進行感知,前面 MutableSnapshot 的定義中看到 readObserverwriteObserver 成員,它們就是快照上對狀態進行讀寫操作時的回調。

val state = mutableStateOf(1)

// 監聽狀態讀操作
val readObserver: (Any) -> Unit = { readState ->
    if (readState == state) {
        println("readObserver: $readState") // 打印 2
    }
}
// 監聽狀態寫操作
val writeObserver: (Any) -> Unit = { writtenState ->
    if (writtenState == state) {
        println("writeObserver: $writtenState") // 打印 2
    }
}

val snapshot = Snapshot.takeMutableSnapshot(
    readObserver = readObserver,
    writeObserver = writeObserver
)

snapshot.enter {
    // 寫操作,觸發 writeObserver 回調
    state.value = 2 

    // 讀操作,觸發 readObserver 回調
    val value = state.value 

    println(value) // 打印 2
}
snapshot.apply()

snapshot.dispose()

上面代碼中,我們在創建快照時傳入讀寫回調,快照中讀寫狀態時依次觸發回調,因此上面代碼的日志輸出如下:

writeObserver: 2
readObserver: 2
2

快照對狀態讀寫的感知是 Compose 狀態更新后自動觸發重組的基礎,我們在后文會詳細介紹。

5. 全局快照

我們知道 GlobalSnapshot 是程序所處的默認快照,它也是所有快照的 Root。由于不再存在父快照,所以全局快照上對狀態的修改不需要追加提交操作(apply),作為 Root 它更重要的職責是“被提交”。全局快照上的狀態變化通常是通過子快照的提交發生的,就如同 Main 上的代碼變動大多來自各分支的 MR 。

監聽全局狀態變化

子快照上的狀態修改最終會通過 apply 提交到父快照。registerApplyObserver 可以監聽子快照提交后的狀態變化。Compose 組合階段的代碼都執行在子快照上,所以組合階段的狀態變化都可以通過 ApplyObserver 獲取。

提示: Composae 渲染分有三個階段:組合,布局,繪制,文中提到的組合就是其中第一個階段

developer.android.google.cn/jetpack/com…

有些狀態變化發生在組合階段之外,比如 onClick 或者一個異步請求的返回都可能觸發狀態變化,組合之外的代碼不執行在子快照,因此它們會直接在全局快照上修改狀態。全局快照上沒有 apply 操作,但是我們通過主動調用 Snapshot.sendApplyNotifications() 同樣可以向 ApplyObserver 發送通知獲知全局狀態的修改。sendApplyNotifications 通過升級全局快照 id 來確定需要通知哪些狀態的變化,即自上次升級 id 以來的所有狀態

ApplyObserver 的通知可能來自子快照的提交,也可能來自 sendApplyNotifications 的直接調用,但用途都是為了監聽全局狀態的變化。

下面的例子展示了 sendApplyNotifications 的使用效果

val state = mutableStateOf(1)

Snapshot.registerApplyObserver { set, _ ->
    // 將響應 sendApplyNotifications 的調用

    // 獲取有變更的狀態
    println("$set") // [MutableState(value=3)]
}

state.value = 2
state.value = 3 // 向 ApplyObserver 通知最后一次變化

// 通知變化
Snapshot.sendApplyNotifications()

除了使用 ApplyObserver 監聽全局變化,我們還可以監聽全局快照上對單個狀態的寫操作,由于全局快照不使用 takeSnapshot 創建,無法通過傳入 writeObserver 注冊回調,全局快照的寫回調通過使用 Snapshot.registerGlobalWriteObserver 注冊:

val state = mutableStateOf(1)

val observer = Snapshot.registerGlobalWriteObserver { writtenState ->
    // MutableState(value=2) 和 MutableState(value=3) 都會收到
    println("$writtenState") 
}

state.value = 2
state.value = 3

observer.dispose()

每次狀態修改都可以通過 registerGlobalWriteObserver 監聽。注意全局快照不提供讀操作的回調注冊,因為 Compose 只會在組合階段追蹤對狀態的讀取,所以在子快照監聽足以。

非 Compose 中使用快照

文章開頭就提到,Compose 快照系統可以脫離 Compose UI 單獨使用。下面的例子中,我們通過監聽全局快照的狀態,實現基于 View 的狀態管理。

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    private var counter by mutableStateOf(0)

    private val observer = Snapshot.registerGlobalWriteObserver {
        Snapshot.sendApplyNotifications()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch {
            snapshotFlow {
                // 將 Counter 的變化更新至 TextView
                binding.textCounter.text = "$counter"
            }.collect()
        }

        binding.buttonIncrement.setOnClickListener {
            counter++
        }
        binding.buttonDecrement.setOnClickListener {
            counter--
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        observer.dispose()
    }
}

snapshotFlow 是 Compose 提供的狀態管理 API ,可以監聽全局快照的狀態變化并轉化為 Flow 發送出去。具體實現我們就不看了,只需要知道它內部通過 ApplyObserver 觀察狀態變化,因此我們通過 registerGlobalWriteObserver 監聽到狀態修改后,通過 sendApplyNotifications 發送通知。

這段代碼同時也揭示了 Compose 的 State 可以像 RxJava/LiveData/Flow 那樣成為一種通用的響應式工具,而且還可以省掉冗余的 subscribe/observe/collect 代碼,snapshotFlow { } 中會自動追蹤所有被讀取的狀態,當它們發生變化時,block 會觸發執行,響應式邏輯更加簡潔。

6. 并發與沖突解決

前面的例子都是跑在單線程中的,而作為一個 MVCC 系統,只有在并發場景中使用才更有意義。通常并發環境下對數據訪問,為了保證線程安全需要添加各種讀寫鎖,而快照系統通過訪問隔離實現無鎖操作,提高并發性能。此外快照的提交機制也保證了容錯性,進一步套用數據庫事務的說法就是保證了 ACID 中的原子性、隔離性和一致性

多線程下的快照保存

當快照在多線程環境下使用時,當前快照信息保存在 ThreadLocal 中的。Compose 在組合執行過程中,通過 currentSnapshot() 獲取當前快照

//androidx.compose.runtime.SnapshotThreadLocal

//如果不存在當前快照,則返回全局快照
internal fun currentSnapshot(): Snapshot =
    threadSnapshot.get() ?: currentGlobalSnapshot.get()
    
private val threadSnapshot = SnapshotThreadLocal<Snapshot>()

//使用 ThreadLocal 管理快照
internal actual class SnapshotThreadLocal<T> {
    private val map = AtomicReference<ThreadMap>(emptyThreadMap)
    private val writeMutex = Any()

    @Suppress("UNCHECKED_CAST")
    actual fun get(): T? = map.get().get(Thread.currentThread().id) as T?

    actual fun set(value: T?) {
        val key = Thread.currentThread().id
        synchronized(writeMutex) {
            val current = map.get()
            if (current.trySet(key, value)) return
            map.set(current.newWith(key, value))
        }
    }
}

單線程中同時只有一個快照處于活動中,活動中的快照通過 SnapshotThreadLocal 保存在 ThreadLocal 中,Compose 在組合階段通過 currentSnapshot() 可以獲取當前線程的活動快照。活動快照 dispose 后從 ThreadLocal 移走,之前非活動的快照進入活動狀態。 從 Snapshot#enter 方法的實現可知,進入快照的本質就是將快照存入 SnapshotThreadLocal

inline fun <T> enter(block: () -> T): T {
    val previous = makeCurrent()
    try {
        return block()
    } finally {
        restoreCurrent(previous)
    }
}

internal open fun makeCurrent(): Snapshot? {
    val previous = threadSnapshot.get()
    threadSnapshot.set(this)
    return previous
}

mergeRecords 解決沖突

并發環境必然要考慮沖突的發生。當我們在子線程快照中修改了某 StateObject,同時它在父快照中也發生了變化,那么當提交子快照時就會遇到沖突,此時就要像 git merge 沖突一樣,要么放棄提交,要么對沖突進行解決。記得前面 StateObject 的類圖中曾經出現了一個 mergeRecords 方法,StateObject 就是用它來處理狀態沖突的:

//androidx/compose/runtime/SnapshotState.kt

override fun mergeRecords(
    previous: StateRecord, // 子快照創建之前的全局狀態
    current: StateRecord, // 全局快照最新狀態
    applied: StateRecord // 待提交的子快照狀態
): StateRecord? {
    val previousRecord = previous as StateStateRecord<T>
    val currentRecord = current as StateStateRecord<T>
    val appliedRecord = applied as StateStateRecord<T>
    //父快照與待提交子快照的狀態比較
    return if (policy.equivalent(currentRecord.value, appliedRecord.value))
        current
    else {//如果狀態不相等,進行merge操作
        val merged = policy.merge(
            previousRecord.value,
            currentRecord.value,
            appliedRecord.value
        )
        if (merged != null) {//merge成功則返回merge結果
            appliedRecord.create().also {
                (it as StateStateRecord<T>).value = merged
            }
        } else {
            null
        }
    }
}

當子快照提交時,對全局快照的 previouscurrent 會進行比較,如果不相等則意味著本次提交有沖突的可能,此時會通過 mergeRecords 解決沖突,進入上面的代碼。邏輯很清晰,重點是對 policy 的兩個方法調用,equivalent 用來比較 currentapplied,如果不相等則調用 merge 進行合并操作,解決沖突。

policy 是一個 SnapshotMutationPolicy 對象,代表快照沖突時的解決策略,我們使用 mutableStateOf 創建狀態時可以傳入自定義 Policy,Compose 也提供了三個默認 Policy,它們的區別主要是 equivalent 的不同:

  • structuralEqualityPolicy:結構化比較,即通過 == 比較狀態值是否相等,這也是 SnapshotState 目前默認的策略
  • referentialEqualityPolicy – 引用比較,通過 === 比較,只有同一實例才相等
  • neverEqualPolicy :永遠判定為不相等

以上無論哪種 Policy 在 merge 的默認實現上都一樣,即不合并,狀態提交失敗。因為 merge 本身屬于業務范疇,很難給出默認實現,需要開發者根據需要自己實現。

注意:當我們更新 StateObject 時,需要判斷是否發生變化以決定是否應該重組,這個判斷也是使用 SnapshotMutationPolicy#equivalent 完成的。

7. 如何支持 Compose 重組?

前面講的那么多,基本都是圍繞快照系統自身的工作原理在做介紹,甚至展示了快照在非 Compose 場景的使用。那么回歸 Compose 的主題,快照是如何對 Compose UI 提供幫助的呢?快照對于 Compose UI 的最主要意義是支持了重組機制的運行,這得益于也正是得益于前文介紹過的兩個特點:讀寫感知 & 讀寫隔離

讀寫感知:標記 RecomposeScope

我們知道 Compose 通過狀態變化驅動重組進而完成 UI 的刷新,而且 Compose 的重組是“智能的”,遵循范圍最小化原則。每個返回 Unit@Composable 函數(或 lambda)都是一個 RecomposeScope,Scope 會追蹤內部訪問的狀態,當狀態發生變化時該 Scope 會參與重組,如果狀態無變化則會跳過重組。這整個過程正是依靠快照讀寫感知的機制實現的。

Compose 通過調用 Recomposer#composing 方法完成組合。

//androidx.compose.runtime.Recomposer
private inline fun <T> composing(
    composition: ControlledComposition,
    modifiedValues: IdentityArraySet<Any>?,
    block: () -> T
): T {
    //創建快照
    val snapshot = Snapshot.takeMutableSnapshot(
        readObserverOf(composition), writeObserverOf(composition, modifiedValues)
    )
    try {
        // 進入快照
        return snapshot.enter(block)
    } finally {
        applyAndCheck(snapshot)
    }
}

可以看到,組合開始時先創建了一個可變快照,并調用 readObserverOfwriteObserverOf 創建狀態讀寫回調傳入傳入快照。接著調用 enter 進入快照執行組合階段的 Composable 函數,所以 Composalbe 在快照上的狀態讀寫都會被監聽到。

Composable 中讀取狀態時觸發回調,最終調用到 recordReadOf,將修改的 StateObject 連同 currentRecomposeScope 一并注冊到 observationsobservations 記錄了哪些 Scope 訪問了哪些 State。

 override fun recordReadOf(value: Any) {
        if (!areChildrenComposing) {
            composer.currentRecomposeScope?.let {
                it.used = true
                observations.add(value, it)
                ...
            }
        }
    }

當 Composable 對狀態進行寫入時調用 recordWriteOf 方法,從 observations 中找到關聯的 Scope 標記為 invalid。

 override fun recordWriteOf(value: Any) = synchronized(lock) {
        invalidateScopeOfLocked(value)

        derivedStates.forEachScopeOf(value) {
            invalidateScopeOfLocked(it)
        }
    }

  private fun invalidateScopeOfLocked(value: Any) {
          observations.forEachScopeOf(value) { scope ->
            if (scope.invalidateForResult(value) == InvalidationResult.IMMINENT) {
                observationsProcessed.add(value, scope)
            }
        }
    }

在下次幀信號到達時,invalid 的 scope 會在重組中執行,基于最新狀態完成組合,同時重復上述過程,設置監聽感知狀態的下一次變化。

全局快照上的狀態修改發生在組合階段以外,但同樣可以確定 RecomposeScope,這是通過前面講 registerApplyObserver 實現的。當全局快照中發生狀態寫操作時,GlobalSnapshotManager 會發送 SendApplyNotification

//androidx.compose.runtime.Recomposer#recompositionRunner
val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
    synchronized(stateLock) {
        if (_state.value >= State.Idle) {
            snapshotInvalidations += changed
            deriveStateLocked()
        } else null
    }?.resume(Unit)
}

如上,RecomposerApplyObserver 中獲得變化的狀態 changed,然后調用 deriveStateLocked() 方法,最終也會執行 invalidateForResult 找到 changed 關聯的 Scope 并標記為 invalid。

讀寫隔離:支持重組并行化

官方文檔告訴我們重組是并行的:

Compose can optimize recomposition by running composable functions in parallel. This lets Compose take advantage of multiple cores.

但截至目前重組仍然跑在單線程上,并行化還在開發中,但是依托快照系統并行化重組隨時可能開啟,所以我們現在就需要帶著并行的意識開發自己的代碼,避免屆時出現 Bug。重組的并行化得益于快照的隔離機制,重組在執行過程中,不會受到其它線程對狀態修改的影響,杜絕并發異常的發生。

結合下面的時序圖,我們梳理一下 Compose 重組的整個過程,看看快照在其中是如何發揮作用的。假定場景是在 onClick 中修改了某個狀態,且并行化已啟動。如前文所述,onClick 的狀態修改發生在全局快照

注意:圖中的箭頭并非源碼中真實的方法調用,只表示一個依賴關系

  1. 全局快照的狀態變化會通過 sendApplyNotifications 通知出來
  2. Recomposer 接收到變化的狀態,在下一幀到來之前將需要重組的 Scope 標記為 invalid
  3. 當幀信號達到時,Recomposer 查找 invalid 的 Scope,獲取空閑子線程并創建快照,在快照上執行 Scope 代碼
  4. Scope 代碼執行中如果讀取了某狀態,則作為狀態的觀察者記錄到 observations
  5. Scope 內部如果對某狀態進行了修改,則從 observations 查找觀察者狀態,標記為 invalid。
  6. Scope 執行結束后,如果期間狀態有修改,則通過快照提交,將狀態變化同步給全局。
  7. 全局狀態變化通過 ApplyObserver 回調 Recomposer,然后重復過程 2。

8. 回顧&總結

以上就是快照的基本工作原理以及其支持重組的整個過程。最后讓我們回顧一下本文開頭的幾個問題,鞏固所學的內容:

  • 快照能做什么?

Compose 快照是一個可以感知狀態讀寫的 MVCC 系統,它主要功能是隔離和感知狀態的變化。

  • 快照與狀態的關系?

快照隔離和感知的對象是狀態,狀態通過 snapshotId 與快照建立關聯,實現訪問隔離。

  • 快照與線程的關系?

快照可以在單線程下運行,但是它更適合在并發環境下使用,快照幫助多線程任務實現線程安全

  • 快照與重組的關系?

Compose 的重組借助快照實現了并發執行,同時通過快照的讀寫感知確定參與下次重組的范圍。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容