iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0
Mobile Development

認真學 Compose - 對 Jetpack Compose 的問題與探索系列 第 25 篇

D25 / 為什麼 State 改變會觸發 recomposition - State & Snapshot system

  • 分享至 

  • xImage
  •  

今天大概會聊到的範圍

  • Snapshot system

上一篇有提到,State 改變時會觸發 recomposition。視這個行為是一種定義。但是為什麼?這怎麼發生的呢?

今天要介紹的是 Compose 中另一個與 State 息息相關的概念 - Snapshot

什麼是 Snapshot

Snapshot 可以想像是當下所有 State 的存檔、快照。就像遊戲存檔或是電腦備份一樣,當下是什麼就是什麼,全部存起來。

提醒:Snapshot 通常不會用在 Compose 的開發中,主要是在用於 Compose 的內部運作。

讓我們看看 Snapshot 實際上怎麼使用的:

fun main() {

    // 1. 
    val data = mutableStateOf("") 
    
    // 2.
    data.value = "Foo"
    println("2: ${data.value}")
    
    // 3.
    val snap = Snapshot.takeSnapshot()
    
    // 4.
    data.value = "Bar"
    println("4: ${data.value}")
    
    // 5. 
    snap.enter {
        println("5: ${data.value}")
    }

    // 6.
    println("6: ${data.value}")
    
    
    // 7.
    snap.dispose()
}

Snapshot 和 State 的運作,是不一定要在 composable function 中執行的

Output:

2: Foo
4: Bar
5: Foo
6: Bar
  1. 透過 mutableStateOf 建立 State。
  2. 編輯 State 的 value 後,印出來的也不意外是 "Foo"
  3. 在這時我們拍一張 sanpshot,將當下的 State 通通存起來
  4. 再次編輯 State 的 value,並可以看到 State 的確有被我們修改
  5. 透過 Snapshot.enter 可以 access 當時拍的 snapshot,在這個 scope 內,我們可以取得當時的 state
  6. 在 snapshot 外,state 依然是編輯後的 Bar,並不會因為我們進去 snap 就改變
  7. 使用完 snapshot 機制後,要將其 dispose 釋放掉資源

Snapshot 的概念就是這麼單純:保留某一個時刻的所有 State。

MutableSnapshot

在 enter snapshot 後,如果我們還想對 State 進行編輯,將會得到錯誤。

    snap.enter {
        data.value = "Buzz"  // << --- Error
    }

在 snapshot 中,State 是唯讀無法編輯的。若希望編輯,我們會需要 MutableSnapshot

fun main() {
    val data = mutableStateOf("")
    
    data.value = "Foo"
    
    // 1.
    val mutableSnap = Snapshot.takeMutableSnapshot()
    
    // 2.
    mutableSnap.enter {
        println("2: ${data.value}")
        data.value = "Buzz"
        println("3: ${data.value}")
    }
    
    
    println("4: ${data.value}")
    
    // 5.
    mutableSnap.enter {
        println("5: ${data.value}")
    }
    
    // 6.
    mutableSnap.apply()
    println("6: ${data.value}")
    
    mutableSnap.dispose()
}
Output:

2: Foo
3: Buzz
4: Foo
5: Buzz
6: Buzz
  1. 我們使用 takeMutableSnapshot 來建立 MutableSnapshot
  2. 在 MutableSnapshot 中,我們一樣可以拿到 takeSnapshot 時的資料 "Foo"
  3. 在 MutableSnapshot 中,我們可以對 Sate 進行編輯
  4. 但這個編輯不會影響到外部的 State
  5. 再次進入 snapshot,可以再次拿到曾經在 snapshot 中編輯的 State,就樣平行時空一樣。
  6. 透過 MutableSnapshot.apply() 我們可以將 snapshot 的值實際賦予到 State 身上
  7. 注意,如果再 takeMutableSnapshot 和 apply 之間,有人直接對 State set value,apply 也不會發生作用

監聽 takeMutableSnapshot

fun takeMutableSnapshot(
    readObserver: ((Any) -> Unit)? = null,
    writeObserver: ((Any) -> Unit)? = null
): MutableSnapshot =
    (currentSnapshot() as? MutableSnapshot)?.takeNestedMutableSnapshot(
        readObserver,
        writeObserver
    ) ?: error("Cannot create a mutable snapshot of an read-only snapshot")

其實仔細看看 takeMutableSnapshot 的 signature,會發現其實他還有吃兩個參數:read/write 的 Observer。
實際上我們可以透過這兩個角色去監聽到資料被寫入、被讀取的時機,並且取得即將被異動的 State 與其資料。

fun main() {
    val data = mutableStateOf("")
    
    data.value = "Foo"
    
    val readObserver = { readState: Any -> if (readState == data) println("READ")}
    
    val writeObserver = { readState: Any -> if (readState == data) println("WRITE")}
    
    val mutableSnap = Snapshot.takeMutableSnapshot(readObserver, writeObserver)
    
    mutableSnap.enter {
        println("1: ${data.value}")
        data.value = "Buzz"
        println("2: ${data.value}")
    }
    
    println("3: ${data.value}")
    
    mutableSnap.apply()
    println("4: ${data.value}")
    
    mutableSnap.dispose()
}
READ
1: Foo
WRITE
READ
2: Buzz
3: Foo
4: Buzz

在 println 前,我們需要去讀取 data.value 並將資料喂給 println function ,因此每次在 snapshot 內的 println 之前都會觸發 READ

回頭說說 Recomposition

Composable 最後會被丟到 Recomposer 去執行,在 Recomposer 中我們可以發現這段 code

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)
    }
}

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


// 

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

source

Recomposer 在 composing 時,利用 snapshot 的 read/write observer 關注在 snapshot 中的 state 變化。並且在 read observer 時建立 recompose scope。

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


override fun recordWriteOf(value: Any) = synchronized(lock) {
    // ...
    derivedStates.forEachScopeOf(value) { // <--- 
        invalidateScopeOfLocked(it)
    }
}

在資料變動時,會 iterate 各個 scope 並且將他們 invalidate。


今天聊到的東西非常的底層,因為篇幅的關係,有些邏輯沒有說明到。非常推薦大家到下面 Reference 的地方閱讀本篇主要參考的文章。看了這些之後感覺對整個 compose 運作流程又再更近一步的認識了!


Reference:


上一篇
D24 / 什麼時候我的 Composable function 會重新被呼叫 - recompose
下一篇
D26 / 要怎麼放 Compose 進我的專案? - Compose View
系列文
認真學 Compose - 對 Jetpack Compose 的問題與探索 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言