揭祕 Jetpack Compose 快照系統 | 開發者説·DTalk

語言: CN / TW / HK

本文原作者: fundroid, 原文 發佈於: AndroidPub

引言

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

  • 快照能做什麼?

  • 快照與狀態的關係?

  • 快照與線程的關係?

  • 快照與重組的關係?

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

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

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

快照的基本操作

快照並非 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 永遠是從當前快照提交到 "父快照"。快照上允許嵌套創建快照,因此快照存在父子關係。

訪問隔離的實現原理

前面介紹了快照的基本功能是對狀態的訪問隔離。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 位置也有着異曲同工之處。

狀態讀寫感知

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

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 狀態更新後自動觸發重組的基礎,我們在後文會詳細介紹。

全局快照

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

監聽全局狀態變化

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

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

http://developer.android.google.cn/jetpack/compose/phases

有些狀態變化發生在組合階段之外,比如 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 會觸發執行,響應式邏輯更加簡潔。

併發與衝突解決

前面的例子都是跑在單線程中的,而作為一個 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
}
}
}

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

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

  • structuralEqualityPolicy: 結構化比較,即通過 == 比較狀態值是否相等,這也是 SnapshotState 目前默認的策略

  • referentialEqualityPolicy – 引用比較,通過 === 比較,只有同一實例才相等

  • neverEqualPolicy: 永遠判定為不相等

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

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

如何支持 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)
}
}

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

Composable 中讀取狀態時觸發回調,最終調用到 recordReadOf ,將修改的 StateObject 連同 currentRecomposeScope 一併註冊到 observations observations 記錄了哪些 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)
}

如上, Recomposer ApplyObserver 中獲得變化的狀態 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。

回顧&總結

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

  • 快照能做什麼?

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

  • 快照與狀態的關係?

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

  • 快照與線程的關係?

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

  • 快照與重組的關係?

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

長按右側二維碼

查看更多開發者精彩分享

"開發者説·DTalk" 面向 中國開發者們徵集 Google 移動應用 (apps & games) 相關的產品/技術內容。歡迎大家前來分享您對移動應用的行業洞察或見解、移動開發過程中的心得或新發現、以及應用出海的實戰經驗總結和相關產品的使用反饋等。我們由衷地希望可以給這些出眾的中國開發者們提供更好展現自己、充分發揮自己特長的平台。我們將通過大家的技術內容着重選出優秀案例進行 谷歌開發技術專家 (GDE) 的推薦。

  點擊屏末  |  閲讀原文  | 即刻報名參與  " 開發者説 · DTalk"