繼續(xù)深挖,Jetpack Compose的State快照系統(tǒng)

Jetpack Compose 有一種特殊的方式來表示狀態(tài)和傳播狀態(tài)變化,從而驅(qū)動最終的響應(yīng)式體驗:狀態(tài)快照系統(tǒng)(State snapshot system)。這種響應(yīng)式模型使我們的代碼更加強(qiáng)大和簡潔,因為它允許組件根據(jù)它們的輸入自動重組,并且只在必要時重組,避免了我們過去在Android View 系統(tǒng)中手動通知這些更改所需的所有樣板文件。

什么是 Snapshot State

Snapshot state(快照狀態(tài))是指可以被記錄并觀察其變化的隔離狀態(tài)。當(dāng)我們調(diào)用像mutableStateOf、mutableStateListOf、mutableStateMapOf、derivedStateOf、produceState、collectAsState等函數(shù)時,我們所得到的狀態(tài)就是快照狀態(tài)。所有這些函數(shù)都返回某種類型的狀態(tài),開發(fā)人員經(jīng)常稱其為快照狀態(tài)。

Snapshot state” 這個術(shù)語的命名是因為它是 Jetpack Compose runtime 定義的狀態(tài)快照系統(tǒng)的一部分。這個系統(tǒng)建模和協(xié)調(diào)狀態(tài)變化和變化傳播。它是以分離的方式編寫的,因此理論上可以被其他想要依賴于可觀察狀態(tài)的庫使用。

關(guān)于變化傳播,我們在之前了解到的一件事情是所有 Composable 聲明和表達(dá)式都會被 Jetpack Compose 編譯器包裝,以自動跟蹤其體內(nèi)的任何快照狀態(tài)讀取。這就是快照狀態(tài)如何被(自動)觀察的方式。目標(biāo)是每當(dāng) Composable 讀取的狀態(tài)發(fā)生變化時,runtime 就會使其RecomposeScope 失效,以便在下一次重組時再次執(zhí)行它。

這是由 Compose 提供的基礎(chǔ)設(shè)施代碼,因此它不需要存在于任何客戶端代碼庫中。runtime 的客戶端,如 Compose UI,可以完全不需要了解失效和狀態(tài)傳播的方式,或者如何觸發(fā)重組,而只需要關(guān)注提供與該狀態(tài)配合使用的構(gòu)建塊:即 Composable 函數(shù)。

但是快照狀態(tài)不僅僅是自動通知更改以觸發(fā)重組的問題。使用 “snapshot” 這個單詞的一個很重要的原因是:狀態(tài)隔離。這代表了我們在并發(fā)上下文中應(yīng)用的隔離級別。

想象一下在不同線程之間處理可變狀態(tài)會怎樣。這很容易變得一團(tuán)糟。需要嚴(yán)格的協(xié)調(diào)和同步來確保狀態(tài)的完整性,因為它可以在同時從不同的線程中讀取或修改。這為沖突、難以檢測的bug和競爭條件敞開了大門。

傳統(tǒng)意義上,編程語言以不同的方式處理這個問題,其中之一是不可變性。不可變數(shù)據(jù)在創(chuàng)建后永遠(yuǎn)不會被修改,這使它在并發(fā)場景下絕對安全。另一個有效的方法可以是 actor 系統(tǒng)。該系統(tǒng)專注于跨線程的狀態(tài)隔離。Actor 保留其自己的狀態(tài)副本,通過消息實現(xiàn)通信/協(xié)調(diào)。如果該狀態(tài)是可變的,則需要存在一些協(xié)調(diào)來使全局程序狀態(tài)一致。Compose 快照系統(tǒng)不是基于 actor 系統(tǒng),但實際上更接近于該方法。

Jetpack Compose 使用可變狀態(tài),因此 Composable 函數(shù)可以自動響應(yīng)狀態(tài)更新。僅使用不可變狀態(tài)的庫是沒有意義的。這意味著它需要解決在并發(fā)場景中共享狀態(tài)的問題,因為組合可以在多個線程中實現(xiàn)。Compose解決此問題的方法就是狀態(tài)快照系統(tǒng),它基于狀態(tài)隔離和后續(xù)的變更傳播,以便可以在多個線程之間安全地使用可變狀態(tài)。

快照狀態(tài)系統(tǒng)是使用并發(fā)控制系統(tǒng)建模的,因為它需要以安全的方式協(xié)調(diào)跨線程的狀態(tài)。在并發(fā)環(huán)境中共享可變狀態(tài)并不容易,這是一個通用的問題,與庫的實際用例無關(guān)。

在 Jetpack Compose 中 State 是一個接口,任何快照狀態(tài)對象都會實現(xiàn)這個接口。以下是State接口的代碼形式:

這個協(xié)議被標(biāo)記為 @Stable,因為 Jetpack Compose 僅提供和使用穩(wěn)定的實現(xiàn)(出于設(shè)計原因),概括一下,這意味著該接口的任何實現(xiàn)必須確保:

  • 對相同的兩個 State 實例調(diào)用 equals 方法總是返回相同的結(jié)果。

  • 當(dāng)類型的公開屬性值更改時,會通知組合。

  • 它所有的公開屬性值類型也是穩(wěn)定的。

接下來首先了解一些關(guān)于并發(fā)控制系統(tǒng)的知識。這將有助于我們更容易地理解為什么Jetpack Compose狀態(tài)快照系統(tǒng)采用這種模型。

并發(fā)控制系統(tǒng)

狀態(tài)快照系統(tǒng)是按照并發(fā)控制系統(tǒng)實現(xiàn)的,因此讓我們先介紹這個概念。

在計算機(jī)科學(xué)中,“并發(fā)控制”是關(guān)于確保并發(fā)操作正確結(jié)果的一種方法,這意味著協(xié)調(diào)和同步。并發(fā)控制由一系列規(guī)則組成,確保整個系統(tǒng)的正確性。但是,協(xié)調(diào)總是伴隨著一定的代價。協(xié)調(diào)通常會影響性能,因此關(guān)鍵挑戰(zhàn)是設(shè)計一種盡可能高效但不會顯著降低性能的方法。

一個并發(fā)控制的例子是數(shù)據(jù)庫管理系統(tǒng)(DBMS)中的事務(wù)系統(tǒng),這個上下文中的并發(fā)控制確保在并發(fā)環(huán)境中執(zhí)行的任何數(shù)據(jù)庫事務(wù)都是以安全的方式進(jìn)行的,不違反數(shù)據(jù)庫的數(shù)據(jù)完整性。目標(biāo)是維護(hù)正確性。這里的“安全”涵蓋的內(nèi)容包括確保事務(wù)是原子性的、可以安全地撤銷、已提交事務(wù)的效果永遠(yuǎn)不會丟失,以及已中止事務(wù)的效果不會留在數(shù)據(jù)庫中。這是一個復(fù)雜的問題。

并發(fā)控制不僅在DBMS中經(jīng)常出現(xiàn),在編程語言中也會出現(xiàn),例如用于實現(xiàn)事務(wù)內(nèi)存。事務(wù)內(nèi)存試圖通過允許一組加載和存儲操作以原子方式執(zhí)行來簡化并發(fā)編程。實際上,在Compose狀態(tài)快照系統(tǒng)中,當(dāng)狀態(tài)更改從一個快照傳播到其他快照時,狀態(tài)寫入被應(yīng)用為單個原子操作。像這樣分組的操作簡化了并行系統(tǒng)/進(jìn)程中共享數(shù)據(jù)的并發(fā)讀寫之間的協(xié)調(diào)。在此基礎(chǔ)上,原子更改可以輕松地中止、撤銷或重現(xiàn)。即:擁有可重現(xiàn)更改歷史,以可能重新生成程序狀態(tài)的任何版本。

并發(fā)控制系統(tǒng)有不同的類別:

  • 樂觀:不阻塞任何讀或?qū)懖僮鳎@些操作的安全性持樂觀態(tài)度,如果提交時將違反所需規(guī)則,則中止事務(wù)以防止違反。中止的事務(wù)立即重新執(zhí)行,這意味著有開銷。當(dāng)平均中止事務(wù)的數(shù)量不太高時,這種策略可能是一個很好的選擇。

  • 悲觀:如果操作違反規(guī)則,則阻止事務(wù)中的操作,直到違反的可能性消失。

  • 半樂觀:這是其他兩種的混合解決方案。只在某些情況下阻止操作,并對其他情況持樂觀態(tài)度(然后在提交時中止)。

每個類別的性能因因素而異,例如平均事務(wù)完成速率(吞吐量)、所需的并行性水平和其他因素,例如死鎖的可能性。非樂觀類

多版本并發(fā)控制 (MVCC)

Jetpack Compose 中的全局狀態(tài)是跨組合和線程共享的。組合函數(shù)應(yīng)該能夠并行運行(可以隨時進(jìn)行并行重組),如果它們并行執(zhí)行,則可以同時讀取或修改快照狀態(tài),因此需要進(jìn)行狀態(tài)隔離。

并發(fā)控制的主要特性之一實際上是隔離性。該特性確保了在并發(fā)訪問數(shù)據(jù)的情況下的正確性。實現(xiàn)隔離的最簡單方法是阻止所有 readers 直到 writers 完成,但這會對性能產(chǎn)生極大的影響。MVCC(Multiversion concurrency control)可以做得更好。

為了實現(xiàn)隔離性,MVCC 保留了數(shù)據(jù)的多個副本(快照),因此每個線程都可以在給定時刻使用一個隔離的狀態(tài)快照來工作。我們可以將它們理解為狀態(tài)的不同版本(“多版本”)。線程所做的修改對其他線程來說是不可見的,直到所有本地更改完成并傳播。

在并發(fā)控制系統(tǒng)中,這種技術(shù)被稱為“快照隔離”,并且它被定義為用于確定每個“事務(wù)”看到哪個版本的隔離級別。

MVCC 還利用了不可變性,因此每當(dāng)寫入數(shù)據(jù)時,都會創(chuàng)建數(shù)據(jù)的新副本,而不是修改原始數(shù)據(jù)。這導(dǎo)致在內(nèi)存中存儲了相同數(shù)據(jù)的多個版本,就像對象的所有更改歷史一樣。在 Compose 中,這些稱為“狀態(tài)記錄”。

MVCC 還具有的一個特點是它創(chuàng)建了時間點一致的視圖。這通常是備份文件的一個特性,它表示給定備份上所有對象的引用保持一致。在 MVCC 中,通常通過事務(wù) ID 來確保這一點,因此任何讀操作都可以引用相應(yīng)的 ID 來確定使用哪個版本的狀態(tài)。這實際上是 Jetpack Compose 中的工作方式。每個快照都被分配了自己的 ID??煺?ID 是單調(diào)遞增的值,因此它們自然地被排序。由于快照是通過它們的 ID 區(qū)分的,因此讀取和寫入是相互隔離的,無需進(jìn)行鎖定。

Snapshot

一個快照可以在任何時候被創(chuàng)建。它反映了程序在給定時刻(創(chuàng)建快照時)的當(dāng)前狀態(tài)(所有快照狀態(tài)對象)??梢詣?chuàng)建多個快照,它們都會獲得自己獨立的程序狀態(tài)副本。也就是說,當(dāng)前所有快照狀態(tài)對象在那個時間點的狀態(tài)副本(實現(xiàn) State 接口的對象)的副本。

這種方法使得狀態(tài)修改是安全的,因為在一個快照中更新一個狀態(tài)對象不會影響到其他快照中同一個狀態(tài)對象的副本??煺罩g是隔離的。在有多個線程的并發(fā)場景中,每個線程都將指向不同的快照,因此指向不同的狀態(tài)副本。

Jetpack Compose runtime 提供了 Snapshot 類來模擬程序的當(dāng)前狀態(tài)。任何代碼只需要調(diào)用它的靜態(tài)方法:val snapshot = Snapshot.takeSnapshot() 即可獲取到一個快照。這將獲取所有狀態(tài)對象當(dāng)前值的快照,并且這些值將被保留,直到 snapshot.dispose() 方法被調(diào)用。這將決定快照的生命周期。

快照有其生命周期。每當(dāng)我們使用完一個快照時,它都需要被處理掉。如果我們不調(diào)用 snapshot.dispose() ,我們將泄漏所有與該快照相關(guān)的資源及其保留狀態(tài)??煺赵趧?chuàng)建和釋放狀態(tài)之間被視為處于活動狀態(tài)。

當(dāng)一個快照被創(chuàng)建時,它被賦予一個 ID,以便所有在該快照上的狀態(tài)可以輕松地與其他潛在版本的相同狀態(tài)區(qū)分開來。這允許為程序狀態(tài)進(jìn)行版本控制,或者換句話說,根據(jù)版本(多版本并發(fā)控制)使程序狀態(tài)保持一致。

最好的理解快照是通過代碼。我將直接從Zach Klipp的這篇非常值得學(xué)習(xí)且詳細(xì)的帖子中提取一段代碼來說明:

其中 Dog 類的 name 是一個 mutableStateOf("") 的實現(xiàn)。

這里函數(shù) snapshot.enter,通常稱為“進(jìn)入快照”,這會在快照的上下文中運行一個 lambda 表達(dá)式,因此快照成為任何狀態(tài)的真實來源:從 lambda 表達(dá)式讀取的所有狀態(tài)將從快照中獲取其值。這個機(jī)制允許 Compose 和任何其他客戶端庫在給定快照的上下文中運行任何處理狀態(tài)的邏輯。這個過程在本地線程中進(jìn)行,直到調(diào)用 enter 返回。其他任何線程都不會受到任何影響。

在上面的例子中,我們可以看到更新后的狗名為“Fido”,但是如果我們從快照的上下文(enter調(diào)用)讀取它,它會返回“Spot”,這是在快照被創(chuàng)建時它所擁有的值。

當(dāng)然,在使用完快照后必須記得調(diào)用 snapshot.dispose() 來釋放狀態(tài),下面是完整代碼:

class Dog {
    var name: MutableState<String> = mutableStateOf("")
}

fun main() {
    val dog = Dog()
    dog.name.value = "Spot"
    val snapshot = Snapshot.takeSnapshot()
    dog.name.value = "Fido"

    println(dog.name.value) // ---> Fido
    snapshot.enter { println(dog.name.value) } // 進(jìn)入快照 ---> Spot
    println(dog.name.value) // ---> Fido

    // When finished with the snapshot, it must always be disposed. 
    snapshot.dispose()
}

請注意,在enter函數(shù)內(nèi)部,可以根據(jù)快照的類型(只讀 vs 可變)讀取和寫入狀態(tài)。

通過 Snapshot.takeSnapshot() 創(chuàng)建的快照是 只讀 的。它所包含的所有狀態(tài)都不能被修改。如果我們試圖寫入快照中的任何狀態(tài)對象,將會拋出異常。

但并非所有的操作都是讀取狀態(tài),我們可能還需要更新它(寫入)。Compose 提供了 Snapshot 契約的一個特定實現(xiàn):MutableSnapshot,它允許修改它所持有的狀態(tài)。除此之外,還有其他可用的實現(xiàn)。以下列舉了 Snapshot 所有不同類型的實現(xiàn)。

讓我們簡要介紹一下不同類型的快照:

  • ReadonlySnapshot:快照中持有的狀態(tài)對象是只讀的,只能讀取而不能修改。

  • MutableSnapshot:快照中持有的狀態(tài)對象既可以讀取也可以修改。

  • NestedReadonlySnapshot和NestedMutableSnapshot:用于 Child 的只讀和可變快照,因為快照可以形成一棵樹。一個快照可以有任意數(shù)量的嵌套快照。稍后會更詳細(xì)地介紹。

  • GlobalSnapshot:持有全局共享程序狀態(tài)的可變快照。它實際上是所有快照的 root 快照。

  • TransparentObserverMutableSnapshot:這是一個特殊情況。它不應(yīng)用任何狀態(tài)隔離,并且僅存在于在讀取/寫入狀態(tài)對象時通知讀取和寫入觀察者。它上面的所有狀態(tài)記錄都會自動標(biāo)記為無效,因此它們不可被任何其他快照看到/讀取。這種類型的快照的ID始終為其父級的ID,因此為它創(chuàng)建的任何記錄實際上都與父級關(guān)聯(lián)。在這種意義上,它是“透明的”,因為在其中執(zhí)行的所有操作都像在父快照中執(zhí)行一樣。

Snapshot Tree (快照樹)

正如我們上面解釋的那樣,快照形成了一棵樹。因此,我們可以在不同的快照類型中找到 NestedReadonlySnapshot 和 NestedMutableSnapshot。任何快照都可以包含任意數(shù)量的嵌套快照。樹的根是 GlobalSnapshot,保存全局狀態(tài)。

嵌套快照類似于快照的獨立副本,可以獨立銷毀/釋放。這允許在保持父快照處于活動狀態(tài)的同時銷毀/釋放它。它們在Compose中使用子組合(subcomposition)時經(jīng)常出現(xiàn)。

簡短回顧一下。我們之前提到過子組合是在父組合中創(chuàng)建的內(nèi)聯(lián)組合,其唯一目的是支持獨立的失效。組合和子組合也形成了一棵樹。

在創(chuàng)建延遲列表或 BoxWithConstraints 時,會創(chuàng)建嵌套快照的子組合。我們還可以在 SubcomposeLayout 或 VectorPainter 中找到子組合。

當(dāng)需要進(jìn)行子組合時,會創(chuàng)建一個嵌套的快照來存儲和隔離其狀態(tài),因此在子組合消失時嵌套快照可以被銷毀,同時保持父級組合和父級快照處于活動狀態(tài)。對嵌套快照進(jìn)行的任何更改,都將會傳播到其父級。

所有的快照類型都提供了一個函數(shù)來獲取一個嵌套快照并將其附加到父快照上,例如 Snapshot#takeNestedSnapshot() 或 MutableSnapshot#takeNestedMutableSnapshot() 。

一個 Child 只讀快照可以從任何快照類型生成。而可變快照只能從另一個可變快照生成(或從全局快照生成,它也可被視為一個可變快照)。

快照和線程

將快照視為獨立于任何線程范圍之外的結(jié)構(gòu)是非常重要的。線程確實可以有一個當(dāng)前的快照,但是快照不一定與線程綁定。線程可以隨意進(jìn)入和離開快照,另一個線程可以進(jìn)入子快照。實際上,快照的預(yù)期使用案例之一是并行工作??梢援a(chǎn)生多個子線程,每個線程都有自己的快照。

一旦我們定義了可變的快照,我們也將學(xué)習(xí)子快照如何通知父快照其更改以保持一致性。所有線程的更改都將彼此隔離,并且不同線程的沖突更新將被檢測和處理。嵌套快照允許這種工作分解是遞歸的。所有這些都可能解鎖諸如并行組合之類的功能。

可以通過 Snapshot.current 獲取當(dāng)前線程的快照。如果有,則返回當(dāng)前線程的快照;否則返回全局快照(保存全局狀態(tài))。

Compose運行時具有觀察到寫入的狀態(tài)時觸發(fā)重新組合的能力。了解這個機(jī)制與我們之前描述的狀態(tài)快照系統(tǒng)是如何連接的將是有益的。讓我們開始學(xué)習(xí)如何先觀察讀取。

觀察讀寫

Compose runtime 在被觀察的狀態(tài)被寫入時,有能力觸發(fā)重組。

每當(dāng)我們獲取快照時(例如 Snapshot.takeSnapshot()),我們得到的返回值是一個 ReadonlySnapshot 。由于這個快照中的狀態(tài)對象不能被修改,只能被讀取,快照中的所有狀態(tài)都會被保留,直到它被銷毀。takeSnapshot 函數(shù)的 lambda 允許我們傳遞一個 readObserver(作為可選參數(shù))觀察者,每當(dāng)在 enter 調(diào)用中從快照中讀取任何狀態(tài)對象時,都會通知該觀察者。

snapshotFlow 函數(shù)可以作為使用 readObserver 一個例子:fun snapshotFlow(block: () -> T): Flow。該函數(shù)將 State 對象轉(zhuǎn)換為 Flow。當(dāng) Flow 被收集時,它會運行其 block 塊并發(fā)出其中讀取的 State 對象的結(jié)果。當(dāng)其中一個 State 對象被修改時,F(xiàn)low 會將新值發(fā)出給其收集器。為了實現(xiàn)這種行為,它需要記錄所有狀態(tài)讀取,以便在這些狀態(tài)對象中的任何一個發(fā)生更改時重新執(zhí)行 block 塊。為了跟蹤這些讀取,它采用一個只讀快照并傳遞一個讀取觀察者,以便將它們存儲在一個 Set 中:

// SnapshotFlow.kt
fun <T> snapshotFlow(block: () -> T): Flow<T> = flow { 
    val readSet = mutableSetOf<Any>()
    val readObserver: (Any) -> Unit = { readSet.add(it) }
    // ...
    Snapshot.takeSnapshot(readObserver) 
    // ...
    // Do something with the Set
}

只讀快照不僅在讀取某些狀態(tài)時通知其readObserver,還會通知其父級的readObserver。嵌套快照上的讀取必須對所有父級和其觀察者可見,所以快照樹上所有的觀察者都會被通知。

現(xiàn)在讓我們開始觀察寫操作。

觀察寫入也是可能的,因此只有在創(chuàng)建可變快照時才能傳遞 writeObserver(狀態(tài)更新)??勺兛煺帐窃试S修改其持有的狀態(tài)的快照。我們可以通過調(diào)用 Snapshot.takeMutableSnapshot() 來獲取一個可變快照。在這里,我們可以傳遞可選的讀和寫觀察器以便在任何讀取或?qū)懭霑r得到通知。

觀察讀寫的一個好例子就是 Recomposer,它能夠跟蹤 Composition 中的任何讀寫操作,以在需要時自動觸發(fā)重組。

composing 函數(shù)在創(chuàng)建初始組合(Composition)和每次重組時都會調(diào)用。這個邏輯依賴于一個 MutableSnapshot,它允許狀態(tài)不僅可以被讀取,還可以被寫入,并且 enter 調(diào)用的 block 塊中的任何讀取或?qū)懭攵急煌ㄖo組合(Composition)中。(換句話說,也就是可變狀態(tài)的讀寫操作可以被組合追蹤到)

這里作為參數(shù)傳遞的 block 代碼塊,實際上是運行組合或重組的代碼,因此它執(zhí)行樹中的所有 Composable 函數(shù)來計算更改列表。又因為這些操作都是發(fā)生在 enter 函數(shù)內(nèi)部,因此會自動跟蹤任何讀取或?qū)懭氩僮鳌?/p>

每當(dāng)快照狀態(tài)寫入被追蹤到組合中時,讀取完全相同的快照狀態(tài)的相應(yīng)的 RecomposeScopes 將被無效化并觸發(fā)重組。

在組合結(jié)束時,applyAndCheck(snapshot)調(diào)用會將組合期間發(fā)生的任何更改傳播到其他快照和全局狀態(tài)。

下面是觀察者在代碼中的樣子,它們是簡單的函數(shù):

private fun readObserverOf(composition: ControlledComposition): (Any) -> Unit {
    return { value -> composition.recordReadOf(value) }
}

private fun writeObserverOf(composition: ControlledComposition, modifiedValues: IdentityArraySet<Any>?): (Any) -> Unit {
    return { value ->
        composition.recordWriteOf(value)
        modifiedValues?.add(value)
    }
}

有一些比較實用的函數(shù)可以用來在當(dāng)前線程中觀察讀取和寫入。這就是 Snapshot.observe(readObserver, writeObserver, block) 函數(shù)。例如,derivedStateOf 函數(shù)就使用它來響應(yīng)提供的塊中的所有對象讀取。

Snapshot.observe() 是唯一使用 TransparentObserverMutableSnapshot 的地方。創(chuàng)建此類型的父(根)快照的唯一目的是向觀察者通知讀取,如前面所述。Comose團(tuán)隊添加這種類型,是為了避免在一些特殊情況下快照中產(chǎn)生一個回調(diào)列表。

MutableSnapshot

MutableSnapshot 是在處理可變快照狀態(tài)時使用的快照類型,在這種情況下,我們需要跟蹤寫入以自動觸發(fā)重組。

在可變快照中,任何狀態(tài)對象都將擁有快照在被拍攝時的相同的值,除非你在快照中本地修改了狀態(tài)對象。在 MutableSnapshot 中進(jìn)行的所有更改都與其他快照所做的更改隔離。更改從樹的底部向上傳播。子嵌套的可變快照需要先應(yīng)用其更改,然后將其傳播到父級或全局快照(如果它是樹的根)。這是通過調(diào)用 NestedMutableSnapshot#apply 來完成的。(或者 MutableSnapshot#apply,如果是非嵌套的話)

以下段落直接摘自Jetpack Compose runtime 的 kdocs:

Composition uses mutable snapshots to allow changes made in Composable functions to be temporarily isolated from the global state and is later applied to the global state when the composition is applied. If MutableSnapshot.apply fails applying this snapshot, the snapshot and the changes calculated during composition are disposed and a new composition is scheduled to be calculated again.

(翻譯:組合使用可變快照,以便在 Composable 函數(shù)中進(jìn)行的更改在一段時間內(nèi)與全局狀態(tài)隔離,并在應(yīng)用組合時稍后應(yīng)用于全局狀態(tài)。如果 MutableSnapshot.apply 無法應(yīng)用此快照,則該快照和組合期間計算的更改將被丟棄,并計劃重新計算新的組合。)

因此,在應(yīng)用組合(回顧一下:我們應(yīng)用變更是在組合的最后一步通過Applier來完成的)時,將應(yīng)用可變快照中的任何更改并將其通知其父級或最終全局快照。如果應(yīng)用這些更改時出現(xiàn)錯誤,則會安排新的組合。

可變快照也有生命周期。它始終通過調(diào)用 apply 和 dispose 來結(jié)束。這既是將狀態(tài)修改傳播到其他快照的必要條件,也是為了避免泄漏。

通過 apply 傳播的更改應(yīng)用是原子性的(atomically),這意味著全局狀態(tài)或父快照(如果其嵌套)會將所有這些更改視為單個原子更改。這將會清理一下狀態(tài)更改的歷史記錄,以便更容易識別、重現(xiàn)、中止或還原。這就是我們前面在并發(fā)控制系統(tǒng)部分所描述的事務(wù)內(nèi)存的作用。

如果可變快照被丟棄但從未應(yīng)用,則其所有未處理的狀態(tài)更改都將被丟棄。

這里有一個實際的例子,展示了在客戶端代碼中如何使用 apply:

當(dāng)我們從 enter 調(diào)用內(nèi)部打印時,值為 “Another street”,因此修改是可見的。這是因為我們在快照的上下文中運行。但是,如果我們在 enter 調(diào)用之后立即打?。ㄔ谕獠浚?,則該值似乎已恢復(fù)為原始值。這是因為 MutableSnapshot 中的更改與任何其他快照隔離。調(diào)用 apply 之后,更改會傳播,然后我們最終可以看到再次打印 streetname 時輸出的是修改后的值。

注意,只有在 enter 調(diào)用中完成的狀態(tài)更新才會被跟蹤和傳播。

還存在另一種簡化版本的語法:Snapshot.withMutableSnapshot ,它將隱式的確保 apply 會在最后被調(diào)用。

最后調(diào)用 apply 的方式可能會讓我們想起 Composer 是如何記錄和應(yīng)用更改列表的。這又是同樣的概念。每當(dāng)我們需要理解樹中的變更列表時,就需要記錄/延遲這些變更,這樣我們就可以以正確的順序應(yīng)用(觸發(fā))它們,并在那一刻強(qiáng)制一致性。這是程序知道所有更改的唯一時機(jī),或者換句話說,這是它擁有一個全局視角的時刻。

也可以注冊應(yīng)用觀察器來觀察最終的修改更改。這可以通過調(diào)用 Snapshot.registerApplyObserver 來實現(xiàn)。

GlobalSnapshot 和 Nested Snapshot

GlobalSnapshot 是一種可變快照,恰好保存全局狀態(tài)。它將按照上面描述的從下到上的順序從其他快照獲取更新。

GlobalSnapshot 不能嵌套。因為只存在一個 GlobalSnapshot,它實際上是所有快照的最終根。它保存當(dāng)前的全局(共享)狀態(tài)。因此,不能應(yīng)用全局快照(它沒有apply調(diào)用)。

要在全局快照中應(yīng)用更改,它必須是 “advanced” 的。這是通過調(diào)用 Snapshot.advanceGlobalSnapshot() 來完成的,它清除前一個全局快照并創(chuàng)建一個新快照,該快照接受前一個全局快照的所有有效狀態(tài)。在這種情況下,Apply 觀察者也會得到通知,因為即使機(jī)制不同,這些更改也會被有效地“應(yīng)用”。同樣,也不可能對其調(diào)用 dispose()。銷毀全局快照也可以通過“advanced” 的方式完成。

在 Jetpack Compose 中,全局快照是在快照系統(tǒng)初始化期間創(chuàng)建的。在 JVM 中,當(dāng) SnapshotKt.class 被 Java 或 Android runtime 初始化時就會發(fā)生這種情況。

在此之后,在創(chuàng)建 Composer 時會啟動一個全局快照管理器,然后每個組合(包括初始組合和任何進(jìn)一步的重組)創(chuàng)建自己的嵌套可變快照并將其附加到樹中,因此它可以存儲和隔離組合的所有狀態(tài)。Composition 也將利用這個機(jī)會注冊讀寫觀察者來跟蹤對 Composition 的讀寫。還記得前面介紹的 composing 函數(shù)嗎:

最后,任何子組合都可以創(chuàng)建自己的嵌套快照并將其附加到樹中,以在保持父元素活動的同時支持失效。這將為我們提供快照樹的完整藍(lán)圖。

另一個有趣的細(xì)節(jié)是,當(dāng) Composer 被創(chuàng)建后,在創(chuàng)建 Composition 時,會調(diào)用 GlobalSnapshotManager.ensureStarted() 。這是與平臺集成的一部分 (Compose UI),它將開始觀察所有對全局狀態(tài)的寫入,并在 AndroidUiDispatcher.Main 上下文中定時調(diào)度快照應(yīng)用通知。

StateObject 和 StateRecord

多版本并發(fā)控制確保每次寫入狀態(tài)時,都會創(chuàng)建一個新版本(寫時復(fù)制)。Jetpack 組合狀態(tài)快照系統(tǒng)遵循這一點,因此最終可能存儲同一個快照狀態(tài)對象的多個版本。

這種設(shè)計對性能有三個方面的重要意義。

  • 首先,創(chuàng)建快照的成本是 O(1) 復(fù)雜度,而不是 O(N) (其中N是狀態(tài)對象的數(shù)量)。

  • 其次,提交快照的成本是 O(N) 復(fù)雜度,其中 N 是快照中發(fā)生突變的對象的數(shù)量。

  • 第三,快照本身不會持有快照數(shù)據(jù)的列表(只有修改對象的臨時列表),因此狀態(tài)對象可以自由地被垃圾收集器(GC)收集,而不需要通知快照系統(tǒng)。

在內(nèi)部,快照狀態(tài)對象被建模為一個 StateObject,在多版本中,為該對象存儲的每一個版本的存儲形式的都是一個 StateRecord。每條記錄都保存狀態(tài)的單個版本的數(shù)據(jù)。每個快照所看到的版本(記錄)對應(yīng)于拍攝快照時可用的最新有效版本。(快照 ID 最高的有效快照)

但是怎樣才能使?fàn)顟B(tài)記錄有效呢?

“有效” 在特定的快照下才有意義。記錄與創(chuàng)建記錄的快照 ID 相關(guān)聯(lián)。一個快照的狀態(tài)記錄在滿足如下條件時是有效的:如果狀態(tài)記錄的 ID 小于等于快照 ID (即在當(dāng)前或上一個快照中創(chuàng)建的),并且不屬于快照的invalid集合,也沒有被明確標(biāo)記為invalid。前一個快照中的任何有效記錄都會自動復(fù)制到新快照中。

這就引出了一個問題:什么會使記錄成為所提到的無效集合的一部分或顯式標(biāo)記為無效呢?

  • 在當(dāng)前快照之后創(chuàng)建的記錄被認(rèn)為是無效的,因為它們是為在此快照之后創(chuàng)建的快照而創(chuàng)建的。

  • 當(dāng)前快照創(chuàng)建時,如果為快照創(chuàng)建的記錄已打開,則記錄被添加到無效集合中,所以它們也被視為無效。

  • 在應(yīng)用之前被銷毀的快照中創(chuàng)建的記錄被明確標(biāo)記為無效。

一個無效的記錄對任何快照都不可見,因此它無法被讀取。當(dāng)從 Composable 函數(shù)中讀取快照狀態(tài)時,該記錄不會被考慮在內(nèi),而會返回其最新的有效狀態(tài)。

回到狀態(tài)對象。下面是在狀態(tài)快照系統(tǒng)中它們的建模方式的簡要示例。

任何通過任何方式創(chuàng)建的可變快照狀態(tài)對象都將實現(xiàn)此接口。例如,由 mutableStateOf、mutableStateListOf 或 derivedStateOf 運行時函數(shù)返回的狀態(tài)等。

讓我們來看一下 mutableStateOf(value) 函數(shù)。

這個調(diào)用返回一個 SnapshotMutableState 的實例,它本質(zhì)上是一個可觀察的可變狀態(tài),換句話說,它是一個可以更新的狀態(tài),并會自動通知觀察者的狀態(tài)。這個類是一個 StateObject,因此它維護(hù)一個記錄的鏈接列表,用來存儲狀態(tài)的不同版本(在這個例子中是value)。每次讀取狀態(tài)時,將遍歷記錄列表,以找到并返回最新的有效記錄。

如果我們回顧一下 StateObject 的定義,我們可以看到它有一個指針指向記錄鏈表的第一個元素,每個記錄都指向下一個。它還允許在列表中預(yù)先添加一個新記錄(使其成為新 firstStateRecord)。

StateObject 定義中的另一個函數(shù)是 mergeRecords。我們之前提到過系統(tǒng)在可能的情況下可以自動合并沖突。這就是這個函數(shù)的作用。合并策略很簡單,稍后將詳細(xì)介紹。

讓我們稍微了解一下 StateRecord

這里我們可以看到每個記錄都關(guān)聯(lián)了一個快照 ID。這個 ID 是屬于創(chuàng)建該記錄的那個快照的 ID 。這將確定該記錄是否對于遵循上述要求的給定快照是有效的。

我們說過每當(dāng)一個對象被讀取時,會遍歷給定快照狀態(tài)(StateObject)的 StateRecords 列表,查找最新的有效記錄(具有最高的快照 ID)。同樣地,當(dāng)快照被創(chuàng)建時,每個快照狀態(tài)對象的最新有效狀態(tài)都會被捕獲,并且這將是新快照的整個生命周期中使用的狀態(tài)(除非它是可變快照且狀態(tài)在本地被修改)。

StateRecord 還有一個 assign 函數(shù),它從另一個StateRecord對象給其賦值并創(chuàng)建它。

StateRecord 也是一個契約(接口)。每種現(xiàn)有 StateObject 類型定義了不同的實現(xiàn),因為記錄存儲了每種類型的 StateObject 的相關(guān)信息,這些信息對于每個類型(每個使用情況)都不同。

跟隨 mutableStateOf 的例子,我們知道它返回的是一個 SnapshotMutableState,它是一個StateObject。它將維護(hù)一個非常特定類型的記錄鏈表:StateStateRecord。這個記錄只是一個包裝在 T 類型值上的包裝器,因為在這種情況下,這就是我們需要在每個記錄中存儲的所有信息。

另一個好的例子可以是 mutableStateListOf 。它創(chuàng)建了一個 SnapshotStateList,這是 StateObject 的另一個實現(xiàn)。該狀態(tài)模擬了一個可觀察的可變列表(實現(xiàn)了Kotlin集合的MutableList契約),因此其記錄將具有由自身定義的 StateListStateRecord 類型。此記錄使用一個PersistentList(參見Kotlin不可變集合)來保存狀態(tài)列表的一個版本。

讀寫狀態(tài)

換句話說,也就是讀寫狀態(tài)記錄。“當(dāng)讀取一個對象時,將遍歷給定快照狀態(tài)(StateObject)的StateRecords列表,查找最近的有效記錄(具有最高的快照ID)”。讓我們看看這在代碼中是如何實現(xiàn)的。

這是來自 compose.material 庫的 TextField Composable 組件。它會記住一個可變狀態(tài)用于保存文本值,所以每次值更新時,該 Composable 都會重組以在屏幕上顯示新的字符。

我們暫時不考慮 remember 的調(diào)用,因為它不是我們在此討論的重點。這里使用的是 mutableStateOf 函數(shù),用于創(chuàng)建快照狀態(tài):

這最終創(chuàng)建了一個 SnapshotMutableState 狀態(tài)對象,該對象獲取了 value: T 和 SnapshotMutationPolicy<t style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"> 作為參數(shù)。它將包裝該值(存儲在內(nèi)存中),并在需要更新該值時使用沖突策略來檢查所傳遞的 新值 是否與 當(dāng)前值 不同。以下是該類中 value 屬性的定義:</t>

每當(dāng)我們使用 getter 訪問 TextField Composable 內(nèi)部值時(例如 textFieldValueState.value),它將通過下一個狀態(tài)記錄(鏈表中的第一個記錄)的引用 next 來調(diào)用 readable 方法開始迭代。readable 函數(shù)通過迭代查找當(dāng)前(最新的)有效的可讀狀態(tài),同時通知任何已注冊的讀取觀察者。對于每個新迭代項,它將按照先前部分中定義的有效條件進(jìn)行檢查。當(dāng)前快照將是當(dāng)前線程的快照或者全局快照(如果當(dāng)前線程未關(guān)聯(lián)到任何快照的話)。

/**
 * Return the current readable state record for the current snapshot. 
 * It is assumed that [this] is the first record of [state]
 */
fun <T : StateRecord> T.readable(state: StateObject): T {
    val snapshot = Snapshot.current
    snapshot.readObserver?.invoke(state)
    return readable(this, snapshot.id, snapshot.invalid) ?: sync { 
        val syncSnapshot = Snapshot.current
        readable(this, syncSnapshot.id, syncSnapshot.invalid)
    } ?: readError()
}

這就是 mutableStateOf 的快照狀態(tài)是如何讀取的。對于其他可用的可變快照狀態(tài)實現(xiàn)(例如由mutableStateListOf返回的實現(xiàn)),情況也是類似的。

當(dāng)我們想要更新狀態(tài)時,我們可以使用該狀態(tài)的 setter 方法來實現(xiàn)。下面是示例代碼:

withCurrent 函數(shù)在底層調(diào)用了 readable 函數(shù),以便運行提供的代碼塊并將當(dāng)前最新的可讀狀態(tài)記錄作為參數(shù)傳遞給它。

接下來,它會使用提供的 SnapshotMutationPolicy 檢查新值是否等效于當(dāng)前值。如果它們不相等,就會開始寫入過程。這項工作是由 overwritable 函數(shù)完成的。

這里我有意地不深入講解實現(xiàn)細(xì)節(jié),因為它們在未來可能會發(fā)生變化。但是,我會簡要地解釋一下:它使用可寫狀態(tài)記錄運行block塊,并提出一個候選記錄,該候選記錄將是當(dāng)前最新的有效記錄。如果它對于當(dāng)前快照有效,就使用它來進(jìn)行寫入,否則它將創(chuàng)建一個新的記錄并將其添加到列表的開頭,使其成為新的初始記錄。該block塊對其進(jìn)行實際修改。

最后,它會通知任何已注冊的寫觀察器者。

刪除或復(fù)用廢棄的記錄

通過多版本并發(fā)控制,我們可以存儲同一狀態(tài)的多個版本(記錄),但這引入了一個有趣的挑戰(zhàn):刪除已過時且永遠(yuǎn)不會被讀取的版本。我們將在一會兒解釋 Compose 如何解決這個問題,但讓我們先介紹 “打開快照”(open snapshots) 的概念。

任何新的快照都會被添加到一個打開(open)的快照集合中,直到主動關(guān)閉它。在快照保持打開狀態(tài)時,它的所有狀態(tài)記錄都被認(rèn)為對于其他快照無效(不可讀?。?。關(guān)閉快照意味著所有它的記錄自動變?yōu)橛行В勺x取)以供創(chuàng)建任何新的快照使用。

  1. 一旦我們了解了這一點,讓我們來了解一下 Compose 如何回收過時的記錄:

  2. 它跟蹤最低的打開快照。Compose 跟蹤一組打開的快照 ID。這些 ID 是單調(diào)遞增的,不斷增加。

如果一條記錄是有效的但在最低的打開快照中不可見,則可以安全地重用它,因為它永遠(yuǎn)不會被任何其他快照選擇。

復(fù)用被覆蓋的記錄通常會導(dǎo)致可變狀態(tài)對象中只有 1 或 2 個記錄,這會顯著提高性能。隨著快照的應(yīng)用,被覆蓋的記錄將被下一個快照復(fù)用。如果一個快照在應(yīng)用之前被銷毀,則所有記錄都被標(biāo)記為無效(丟棄),這意味著它們可以立即被復(fù)用。

變更傳播

在解釋可變快照中的更改是如何傳播之前,回顧一下“closing”和“advancing”快照的含義可能很有用,以便我們理解這兩個術(shù)語。

關(guān)閉快照實際上就是將其 ID 從打開的快照 ID 集合中刪除,其結(jié)果是所有與該 ID 相關(guān)聯(lián)的狀態(tài)記錄(記錄)將變?yōu)榭梢?可讀,以便由創(chuàng)建的任何新快照讀取。這使得關(guān)閉快照成為傳播狀態(tài)更改的有效方法。

關(guān)閉快照時,很多時候我們希望立即創(chuàng)建一個新的快照來代替它。這就是所謂的 “advancing”。新創(chuàng)建的快照會獲得一個新的 ID,該 ID 通過遞增前一個 ID 生成。然后將此 ID 添加到打開的快照 ID 集合中。

正如我們所學(xué)到的,全局快照永遠(yuǎn)不會應(yīng)用,而總是在前進(jìn),這樣可以使其所有更改對新創(chuàng)建的全局快照可見??勺兛煺找部梢栽谄淝短卓煺諔?yīng)用更改時前進(jìn)。

現(xiàn)在我們已經(jīng)很好地理解了這一點,我們已經(jīng)準(zhǔn)備好學(xué)習(xí)可變快照中的更改是如何傳播的。

在可變快照上調(diào)用snapshot.apply()時,對其范圍內(nèi)的狀態(tài)對象所做的所有局部更改都將傳播到父快照(如果是嵌套可變快照)或全局狀態(tài)。

調(diào)用apply或dispose將劃定快照的生命周期。應(yīng)用的可變快照也可以在之后被釋放。但是,在dispose之后調(diào)用apply將拋出異常,因為這些更改已經(jīng)被丟棄。

根據(jù)我們所描述的,要傳播所有本地更改(對所拍攝的新快照可見),只需從活動快照集中刪除快照就足夠了。無論何時創(chuàng)建快照,當(dāng)前打開的快照的副本都會作為無效快照集傳入(也就是說,任何尚未應(yīng)用的快照都不應(yīng)該對新快照可見)。只需從打開的快照集中刪除快照 id,就足以讓每個新快照將在此快照期間創(chuàng)建的recrods視為有效的,因此,當(dāng)讀取它們對應(yīng)的狀態(tài)對象時,就可以返回它們。

但是只有在確定沒有狀態(tài)沖突(碰撞寫入)之后才應(yīng)該這樣做,因為需要首先解決這些問題。

當(dāng)應(yīng)用快照時,應(yīng)用快照所做的更改將與其他快照的更改一起添加。狀態(tài)對象有一個記錄的鏈表,所有的更改都聚合在其中。這為寫入沖突打開了大門,因為多個快照可能會嘗試對相同的狀態(tài)對象應(yīng)用更改。當(dāng)一個可變快照想要應(yīng)用(通知/傳播)它的本地更改時,它會嘗試檢測潛在的寫沖突并盡可能合并這些沖突。

這里我們有兩個場景:

  • 沒有掛起的本地更改

如果快照中沒有掛起的本地更改:

  • 可變快照被主動關(guān)閉(將其從打開的快照id集中移除,使其所有狀態(tài)記錄對新快照自動可見/可讀)。

  • 全局快照是“advanced”的(與關(guān)閉相同,但也將被創(chuàng)建的新的全局快照所取代)。

  • 利用這個機(jī)會檢查全局快照中是否有任何狀態(tài)更改,這樣可變快照就可以將這些更改通知給任何潛在的應(yīng)用程序觀察者。

  • 有掛起的本地更改

當(dāng)有掛起的更改時:

  • 使用樂觀方法檢測沖突并計算合并的記錄(記住并發(fā)控制類別)。碰撞將嘗試自動合并,否則將被丟棄。

  • 對于每個掛起的本地更改,它都會檢查它是否與當(dāng)前值不同。如果不是,則忽略更改并保持當(dāng)前值。

  • 如果是實際的更改(不同),則檢查已經(jīng)計算的樂觀合并,以決定是保留以前的、當(dāng)前的還是應(yīng)用的記錄。它實際上可以創(chuàng)建所有這些的合并。

  • 如果它必須執(zhí)行記錄的合并,它將創(chuàng)建一個新記錄(不可變性)并將快照id分配給它(將其與可變快照關(guān)聯(lián)),然后將其前置到記錄的鏈表中,使其有效地成為列表中的第一個記錄。

如果在應(yīng)用更改時出現(xiàn)任何失敗,它將回退到?jīng)]有掛起的本地更改時所做的相同流程。這是關(guān)閉可變快照以使其記錄對任何新快照可見,推進(jìn)全局快照(關(guān)閉并用一個新快照替換它),因此它包括剛剛關(guān)閉的可變快照中的所有更改,并通知任何apply觀察者檢測到的任何全局狀態(tài)更改。

對于嵌套的可變快照,過程略有不同,因為它們不會將更改傳播到全局快照,而是傳播到它們的父快照。出于這個原因,它們將其所有已修改的狀態(tài)對象添加到父對象的已修改集。由于所有這些更改都需要由父快照可見,因此嵌套可變快照將自己的id從無效快照的父快照集中刪除。

合并寫沖突

為了進(jìn)行合并,可變快照迭代它的修改狀態(tài)列表(本地更改),對于每一個更改,它執(zhí)行以下操作:

  • 獲取父快照或全局狀態(tài)中的當(dāng)前值(狀態(tài)記錄)。

  • 在應(yīng)用更改之前獲取先前的值。

  • 獲取應(yīng)用更改后對象的狀態(tài)。

  • 嘗試自動合并他們?nèi)齻€。這被委托到狀態(tài)對象中,狀態(tài)對象依賴于提供的合并策略(參見前文的 StateObject 定義)。

事實是 runtime 中沒有一個可用的策略支持正確的合并,因此碰撞更新將導(dǎo)致 runtime 異常并將該問題通知用戶。為了避免陷入這種情況,Compose 通過使用唯一 key 訪問狀態(tài)對象(可組合函數(shù)中記住的狀態(tài)對象通常具有唯一訪問屬性)來保證不可能發(fā)生沖突。給定 mutableStateOf 使用StructuralEqualityPolicy 進(jìn)行合并,它通過等號(==)深度比較對象的兩個版本,因此所有的屬性都會被比較,包括唯一的對象 key ,使得兩個對象不可能碰撞。

自動合并沖突的更改是一個潛在的優(yōu)化,Compose 還沒有使用,但其他庫可以使用。

可以通過實現(xiàn) SnapshotMutationPolicy 這個接口提供一個自定義的沖突策略。從 Compose 文檔中找到的一個可以作為策略參考的例子是,將MutableState作為一個計數(shù)器。該策略假設(shè)將狀態(tài)值更改為相同的不會被視為更改,因此使用 counterPolicy 對可變狀態(tài)的任何更改永遠(yuǎn)不會導(dǎo)致應(yīng)用沖突。

當(dāng)兩個值相同時,它們被認(rèn)為是等效的,因此將保留當(dāng)前值。請注意合并是如何獲得的,將新應(yīng)用的值與前一個值之間的差加到當(dāng)前值之上,因此當(dāng)前值總是反映存儲的總量。

這一段是摘自官方文檔的解釋:*As the name of the policy implies, it can be useful when counting things, such as tracking the amount of a resource consumed or produced while in a snapshot. For example, if snapshot A produces 10 things and snapshot B produces 20 things, the result of applying both A and B should be that 30 things were produced. *(正如策略的名稱所暗示的那樣,它在計數(shù)時非常有用,例如跟蹤快照中消耗或產(chǎn)生的資源的數(shù)量。例如,如果快照A產(chǎn)生了10個產(chǎn)物,快照B產(chǎn)生了20個產(chǎn)物,那么同時應(yīng)用A和B的結(jié)果應(yīng)該是產(chǎn)生了30個產(chǎn)物。)

我們有一個使用計數(shù)器策略進(jìn)行比較的單一可變狀態(tài),以及兩個嘗試修改它并應(yīng)用更改的快照。這將是碰撞的完美場景,但考慮到我們的 counter 策略,任何碰撞都是完全避免的。

這只是如何提供自定義 SnapshotMutationPolicy 以避免沖突的一個簡單示例,因此我們可以了解要點。另一個不可能發(fā)生沖突的實現(xiàn)可能是只能添加元素而不能刪除元素的集合。其他有用的類型(如rope)也可以類似地轉(zhuǎn)換為無沖突的數(shù)據(jù)類型,前提是它們的工作方式和預(yù)期結(jié)果受到一定的限制。

我們還可以提供自定義策略,接受沖突,但通過使用merge函數(shù)合并數(shù)據(jù)來解決沖突。

總結(jié)

Snapshot state 的思想是狀態(tài)隔離、快照隔離。

基于 MVCC(多版本并發(fā)控制) 實現(xiàn)

  • 保留數(shù)據(jù)的多個副本/快照,每個線程在給定時刻使用一個隔離的本地副本來工作。

  • 每個線程都將指向不同的快照,因此指向不同的狀態(tài)副本。

  • 每當(dāng)寫入數(shù)據(jù)時,都會創(chuàng)建數(shù)據(jù)的新副本,而不是修改原始數(shù)據(jù)。

  • 內(nèi)存中存儲了相同數(shù)據(jù)的多個版本,或歷史記錄,Compose中叫狀態(tài)記錄 StateRecord

  • 每個快照都被分配一個 ID,即作為事務(wù) ID,快照 ID 是單調(diào)遞增的。

  • 讀取和寫入根據(jù)快照 ID 區(qū)分,相互隔離,無需進(jìn)行鎖定。

快照生命周期

  • 在調(diào)用 Snapshot.takeSnapshot() 時被創(chuàng)建,在調(diào)用 snapshot.dispose() 時被銷毀??煺赵趧?chuàng)建和釋放狀態(tài)之間被視為處于活動狀態(tài)。

  • 快照不使用時,應(yīng)該被銷毀,否則可能泄漏相關(guān)資源。

snapshot.enter:通常稱為“進(jìn)入快照”,這會在快照的上下文中運行一個 lambda 表達(dá)式,一旦進(jìn)入這個 lambda 中,在其范圍內(nèi)對狀態(tài)的讀取和寫入都是隔離的,基于當(dāng)前快照。它允許在本地線程,與其他線程隔離。

常見類型的快照:

  • ReadonlySnapshot:只讀快照

  • MutableSnapshot:可讀可寫

  • NestedReadonlySnapshot和NestedMutableSnapshot:嵌套快照。用于快照樹中的 Child 的只讀和可變快照??梢栽诒3指缚煺仗幱诨顒訝顟B(tài)的同時獨立銷毀/釋放。進(jìn)行子組合時,會創(chuàng)建一個嵌套的快照。如SubcomposeLayout中。

  • GlobalSnapshot:全局共享狀態(tài)的可變快照。所有快照的 root 快照。它不能嵌套,全局只有一個。

Snapshot Tree:快照可以形成一棵樹,樹的根是 GlobalSnapshot。

快照和線程相互獨立

  • 線程可以有一個當(dāng)前的快照,但是快照不一定與線程綁定。線程可以隨意進(jìn)入和離開一個快照。

  • Snapshot.current 獲取當(dāng)前線程的快照。它返回當(dāng)前線程的快照,或者全局快照。

快照的讀寫監(jiān)聽

  • 如 Snapshot.takeSnapshot(readObserver) 可以為只讀快照設(shè)置一個觀察者。每當(dāng)在 snapshot.enter 調(diào)用中從快照中讀取任何狀態(tài)對象時,都會通知該觀察者。

  • 可變快照可以同時設(shè)置 readObserver 和 writeObserver 觀察讀取和寫入操作。

  • derivedStateOf 函數(shù)的內(nèi)部就是使用 Snapshot.observe(readObserver, writeObserver, block) 來在當(dāng)前線程中觀察讀取和寫入。

Recomposer跟蹤 Composition 中的任何讀寫操作,自動觸發(fā)重組。這是通過向可變快照注冊讀寫觀察者實現(xiàn)的。

  • Snapshot.takeMutableSnapshot(readObserver, writeObserver) 在Recomposer進(jìn)行初始組合和每次重組時都會調(diào)用它。

  • 它在snapshot.enter(block)中運行組合或重組的block代碼,因此可以被監(jiān)聽到。

  • 每當(dāng)快照狀態(tài)寫入被Recomposer追蹤/觀察到時,讀取相同的快照狀態(tài)的相應(yīng)的 RecomposeScopes 將被無效化并觸發(fā)重組。

快照更改的應(yīng)用

  • snapshot.apply() 可以應(yīng)用快照更改(針對可變快照),它是原子性的操作。

  • 調(diào)用 apply() 之后,對快照的修改會傳播到其他快照。對其范圍內(nèi)的狀態(tài)對象所做的所有局部更改都將傳播到父快照(如果是嵌套可變快照)或全局狀態(tài)。

  • Snapshot.withMutableSnapshot{} 是會隱式調(diào)用 apply() 的簡化版本。

全局快照

  • 全局快照的創(chuàng)建是在 SnapshotKt.class 類被JVM初始化時。

  • 創(chuàng)建 Composer 時會啟動一個全局快照管理器,在創(chuàng)建 Composition 時,會調(diào)用 GlobalSnapshotManager.ensureStarted() 將開始觀察所有對全局狀態(tài)的寫入。

  • 每個組合創(chuàng)建自己的嵌套可變快照并將其附加到快照樹中,而全局快照是這個樹的根,因此全局快照管理器可以存儲和隔離組合的所有狀態(tài)。

快照狀態(tài)的內(nèi)部表示

  • MVCC 確保每次寫入狀態(tài)時,都會創(chuàng)建一個新版本(寫時復(fù)制)。

  • 快照狀態(tài)對象的內(nèi)部實現(xiàn)是一個 StateObject,在多版本中,為該對象存儲的每一個版本的存儲形式的都是一個 StateRecord。

  • 每個記錄都保存狀態(tài)的一個版本的信息,每個記錄與創(chuàng)建記錄的快照 ID 相關(guān)聯(lián),快照 ID 是遞增的,這樣每個版本的記錄形成一個按 ID 遞增的單鏈表。

  • 每個快照所看到的版本(記錄)對應(yīng)于拍攝快照時可用的最新有效版本。

  • 最新有效版本是指處于該快照之前創(chuàng)建的記錄且該記錄不是invalid也沒有被加入invalid列表中。

狀態(tài)讀寫

  • 當(dāng)讀取一個對象時,將遍歷給定快照狀態(tài)(StateObject)的StateRecords列表,查找最近的有效記錄(具有最高的快照ID)

  • 寫入時使用當(dāng)前最新的有效記錄作為候選記錄。如果它對于當(dāng)前快照有效,就使用它來進(jìn)行寫入,否則它將創(chuàng)建一個新的記錄并將其添加到列表的開頭,使其成為新的初始記錄。

  • 讀寫完畢后都會通知任何已注冊的讀觀察者和寫觀察器者。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容