揭祕 Jetpack Compose 快照系統 | 開發者説·DTalk
本文原作者: 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 變化如上圖所示:
-
我們在 GlobalSnapshot 中創建子快照,id 賦值為 2;
-
為了讓子快照中訪問不到父快照後續的狀態變化,子快照創建後 GlobalSnapshot 的 id 升級至 3;
-
為了讓 GlobalSnapshot 看不到子快照的狀態變化,將 2 加入 invalid;
-
子快照提交後,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 的狀態修改發生在全局快照。
注意: 圖中的箭頭並非源碼中真實的方法調用,只表示一個依賴關係。
-
全局快照的狀態變化會通過 sendApplyNotifications 通知出來;
-
Recomposer 接收到變化的狀態,在下一幀到來之前將需要重組的 Scope 標記為 invalid;
-
當幀信號達到時,Recomposer 查找 invalid 的 Scope,獲取空閒子線程並創建快照,在快照上執行 Scope 代碼;
-
Scope 代碼執行中如果讀取了某狀態,則作為狀態的觀察者記錄到 observations;
-
Scope 內部如果對某狀態進行了修改,則從 observations 查找觀察者狀態,標記為 invalid;
-
Scope 執行結束後,如果期間狀態有修改,則通過快照提交,將狀態變化同步給全局;
-
全局狀態變化通過 ApplyObserver 回調 Recomposer,然後重複過程 2。
回顧&總結
以上就是快照的基本工作原理以及其支持重組的整個過程。最後讓我們回顧一下本文開頭的幾個問題,鞏固所學的內容:
-
快照能做什麼?
Compose 快照是一個可以感知狀態讀寫的 MVCC 系統,它主要功能是隔離和感知狀態的變化。
-
快照與狀態的關係?
快照隔離和感知的對象是狀態,狀態通過 snapshotId 與快照建立關聯,實現訪問隔離。
-
快照與線程的關係?
快照可以在單線程下運行,但是它更適合在併發環境下使用,快照幫助多線程任務實現線程安全。
-
快照與重組的關係?
Compose 的重組藉助快照實現了併發執行,同時通過快照的讀寫感知確定參與下次重組的範圍。
長按右側二維碼
查看更多開發者精彩分享
"開發者説·DTalk" 面向 中國開發者們徵集 Google 移動應用 (apps & games) 相關的產品/技術內容。歡迎大家前來分享您對移動應用的行業洞察或見解、移動開發過程中的心得或新發現、以及應用出海的實戰經驗總結和相關產品的使用反饋等。我們由衷地希望可以給這些出眾的中國開發者們提供更好展現自己、充分發揮自己特長的平台。我們將通過大家的技術內容着重選出優秀案例進行 谷歌開發技術專家 (GDE) 的推薦。
點擊屏末 | 閲讀原文 | 即刻報名參與 " 開發者説 · DTalk"
- 京東金融Android瘦身探索與實踐
- 京東金融Android瘦身探索與實踐
- Android性能優化-ListView自適應性能問題
- Blazor在IoT領域的前端實踐 @.NET開發者日
- 升級 Android 目標版本到 31(S) 居然這麼多坑
- 如何精準分析特定用户的應用性能問題?這兩個功能您一定要了解
- ShareSDK Android端權限説明
- 適用於Android開發者的多線程總結
- ShareSDK Android端權限説明
- MobPush Android常見問題
- MobPush Android For Unity
- 共碼未來 | 持續賦能開發者和初創生態
- 要近萬元買iPhone 14 Pro才能玩靈動島?Android 開發者:別急,我給你自制了一個 App
- 揭祕 Jetpack Compose 快照系統 | 開發者説·DTalk
- 輕鬆學習,考取證書 | 商品詳情繫列內容第十講
- 是時候讓所有人能一起聊個痛快了!
- 在 Jetpack Compose 中安全地使用數據流
- 共碼未來 | 助力打造現代、高效、流暢的開發體驗
- 共碼未來丨2022 Google 谷歌開發者大會主旨演講亮點回顧
- 京東金融客户端用户觸達方式的探索與實踐