一文看懂 Jetpack Compose 快照系統

語言: CN / TW / HK

theme: devui-blue highlight: mono-blue


1. 引言

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

  • 快照能做什麼?
  • 快照與狀態的關係?
  • 快照與執行緒的關係?
  • 快照與重組的關係?

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

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

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

2. 快照的基本操作

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

快照的建立

先看下面的例子:

```kotlin 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() 建立可寫的快照:

```kotlin // 建立可寫的快照 val snapshot = Snapshot.takeMutableSnapshot()

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

println(state.value) // 列印2

}

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

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

```kotlin snapshot.enter { // ... }

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

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

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

```kotlin 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 中 :

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

//遍歷連結串列,根據 snapshotId 返回符合當前快照讀取條件的 StateRecord private fun 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` 集合中。

```kotlin //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 成員,它們就是快照上對狀態進行讀寫操作時的回撥。

```kotlin 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() ``` 上面程式碼中,我們在建立快照時傳入讀寫回調,快照中讀寫狀態時依次觸發回撥,因此上面程式碼的日誌輸出如下:

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

5. 全域性快照

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

監聽全域性狀態變化

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

提示: Composae 渲染分有三個階段:組合,佈局,繪製,文中提到的組合就是其中第一個階段 https://developer.android.google.cn/jetpack/compose/phases

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

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

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

```kotlin 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` 註冊:

```kotlin 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 的狀態管理。

```kotlin 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() 獲取當前快照

```kotlin //androidx.compose.runtime.SnapshotThreadLocal

//如果不存在當前快照,則返回全域性快照 internal fun currentSnapshot(): Snapshot = threadSnapshot.get() ?: currentGlobalSnapshot.get()

private val threadSnapshot = SnapshotThreadLocal()

//使用 ThreadLocal 管理快照 internal actual class SnapshotThreadLocal { private val map = AtomicReference(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

```kotlin inline fun 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 就是用它來處理狀態衝突的:

```kotlin //androidx/compose/runtime/SnapshotState.kt

override fun mergeRecords( previous: StateRecord, // 子快照建立之前的全域性狀態 current: StateRecord, // 全域性快照最新狀態 applied: StateRecord // 待提交的子快照狀態 ): StateRecord? { val previousRecord = previous as StateStateRecord val currentRecord = current as StateStateRecord val appliedRecord = applied as StateStateRecord //父快照與待提交子快照的狀態比較 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).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 方法完成組合。

kotlin //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。

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

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

```kotlin 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

kotlin //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 的重組藉助快照實現了併發執行,同時通過快照的讀寫感知確定參與下次重組的範圍。

參考

  1. https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn
  2. https://juejin.cn/post/6972692477505437733
  3. https://juejin.cn/post/6974974061466091556
  4. https://juejin.cn/post/6964185100971950093
  5. https://blog.chrnie.com/2021/10/10/Jetpack-Compose-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E4%B9%8B-Snapshot/