Jetpack ComposeのMutableStateが変更されてからdoCompose()までの流れ

なんどもフィールドに入ったり他へ保存を繰り返すので結構しんどいコードリーディングになります。

以下の書き換えが走ったときの挙動を追います。


```kotlin
@Composable
fun Content() {
    var state by remember { mutableStateOf(true) }
    LaunchedEffect(Unit) {
        delay(12000)
        state = false
    }
    if (state) {
        Node1()
    }
    Node2()
}
```

---


今回わかったこと

Recomposer.runRecomposeAndApplyChanges()がだいたいすべてやっている。

runRecomposeAndApplyChanges()が行っていることは1〜4になると思われる。 **0〜2までを今回は読んでいる**。

0: MutableStateが非同期で変更されて、Recomposer.snapshotInvalidationsというフィールドに変更情報が入る

以下runRecomposeAndApplyChanges()内

1: `recordComposerModificationsLocked()` を呼ぶ。Recomposer.snapshotInvalidationsを見て、IdentityScopeMapを使って変更点に対しての影響を受けるスコープを取得し、Recomposer.compositionInvalidationsやComposition.invalidationsというフィールドに変更を入れる。
2: compositionInvalidationsをtoRecomposeに入れる。
3: toRecomposeに対して、performRecompose(composition, modifiedValues)、(内部でdoCompose())することでtoApplyを作成。SlotTableを見て、ここで木を見ていき、木への変更をrecordしていく。 (予想。コード未読)
4: recordされた`toApply`に対してapplyChanges()をすることで、SlotTableの書き換えが走る。 (予想。コード未読)


---

以下は読んでもいいですが、多分大変だと思います。 :sweat: 



まず、set(value) が呼ばれる。

```kotlin:SnapshotMutableStateImpl.kt
internal open class SnapshotMutableStateImpl<T>(
    value: T,
    override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
    @Suppress("UNCHECKED_CAST")
    override var value: T
        get() = next.readable(this).value
        set(value) = next.withCurrent { // ←ここが呼ばれる
            if (!policy.equivalent(it.value, value)) {
                next.overwritable(this, it) { this.value = value } // ←ここを読んでいく
            }
        }
    private var next: StateStateRecord<T> = StateStateRecord(value)
```
---

まず、この中のoverwritableRecord()を読んでいく

```kotlin:Snapshot.kt
internal inline fun <T : StateRecord, R> T.overwritable(
    state: StateObject,
    candidate: T,
    block: T.() -> R
): R {
    var snapshot: Snapshot = snapshotInitializer
    return sync {
        snapshot = Snapshot.current
        this.overwritableRecord(state, snapshot, candidate).block()
    }.also {
        notifyWrite(snapshot, state)
    }
}
```

---


各引数の状態

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/a1de8941-ada3-c89b-5f09-3b8fb7fc5e68.png)


```kotlin
internal fun <T : StateRecord> T.overwritableRecord(
    state: StateObject,
    snapshot: Snapshot,
    candidate: T
): T {
    if (snapshot.readOnly) {
        // If the snapshot is read-only, use the snapshot recordModified to report it.
        snapshot.recordModified(state)
    }
    val id = snapshot.id

    if (candidate.snapshotId == id) return candidate

    val newData = newOverwritableRecord(state, snapshot)
    newData.snapshotId = id

    snapshot.recordModified(state) // ← ここでSnapshotにModifiedという情報をHashMapに保存する

    return newData
}
```

SnapshotにModifiedという情報をHashMapに保存。まだこの時点ではvalue=true

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/9553782f-b14e-1bef-2fce-cd7fc569174e.png)

---


```kotlin
internal inline fun <T : StateRecord, R> T.overwritable(
    state: StateObject,
    candidate: T,
    block: T.() -> R
): R {
    var snapshot: Snapshot = snapshotInitializer
    return sync {
        snapshot = Snapshot.current
        this.overwritableRecord(state, snapshot, candidate).block() // このブロックが呼ばれる
    }.also {
        notifyWrite(snapshot, state)
    }
}
```

---

```kotlin:SnapshotMutableStateImpl.kt
internal open class SnapshotMutableStateImpl<T>(
    value: T,
    override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
    @Suppress("UNCHECKED_CAST")
    override var value: T
        get() = next.readable(this).value
        set(value) = next.withCurrent {
            if (!policy.equivalent(it.value, value)) {
                next.overwritable(this, it) { this.value = value } // このラムダが呼ばれてstateがfalseになる
            }
        }
    private var next: StateStateRecord<T> = StateStateRecord(value)
```

---



```kotlin
internal inline fun <T : StateRecord, R> T.overwritable(
    state: StateObject,
    candidate: T,
    block: T.() -> R
): R {
    var snapshot: Snapshot = snapshotInitializer
    return sync {
        snapshot = Snapshot.current
        this.overwritableRecord(state, snapshot, candidate).block()
    }.also {
        notifyWrite(snapshot, state)  // これでstateがfalseになって、notifyWriteを読んでいく。
```

---


```
androidx.compose.runtime.snapshots.GlobalSnapshot@527a852
MutableState(value=false)@8295418 // 引数のstateがfalse!!!
```

```kotlin:Snapshot.kt
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
    snapshot.writeObserver?.invoke(state)
}
```

commitPending = falseで呼ばれる

```kotlin:GlobalSnapshotManager
    @OptIn(ExperimentalComposeApi::class)
    private val globalWriteObserver: (Any) -> Unit = {
        // Race, but we don't care too much if we end up with multiple calls scheduled.
        if (!commitPending) {
            commitPending = true
            schedule {
                commitPending = false
                Snapshot.sendApplyNotifications()
            }
        }
    }
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/4b92d015-051d-46e3-d201-311f954057af.png)

結果changes=trueになる。


```kotlin:Snapshot.kt
        fun sendApplyNotifications() {
            val changes = sync {
                currentGlobalSnapshot.get().modified?.isNotEmpty() == true
            }
            if (changes)
                advanceGlobalSnapshot() // ここを読んでいく
        }
```

---


```kotlin:Snapshot.kt
private fun advanceGlobalSnapshot() = advanceGlobalSnapshot { }

// つまり今渡っているブロック引数は空。
private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
    val previousGlobalSnapshot = currentGlobalSnapshot.get()
    val result = sync {
        takeNewGlobalSnapshot(previousGlobalSnapshot, block)
    }

    // If the previous global snapshot had any modified states then notify the registered apply
    // observers.
    val modified = previousGlobalSnapshot.modified
    if (modified != null) {
        val observers: List<(Set<Any>, Snapshot) -> Unit> = sync { applyObservers.toMutableList() }
        observers.fastForEach { observer ->
            observer(modified, previousGlobalSnapshot)
        }
    }

    return result
}
```

modifiedはある。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/3659062f-b185-d09e-9b43-ed416e42380c.png)


---

observerはこのラムダ。

```kotlin:Recomposer.kt
            val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
                synchronized(stateLock) {
                    if (_state.value >= State.Idle) { // ここは trueになる
                        snapshotInvalidations += changed // ここで `snapshotInvalidations`のリストに保存する！！！！
                        deriveStateLocked()
                    } else null
                }?.resume(Unit)
            }
```

条件はtrue

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/3c060be1-f590-f831-b70e-527033f9197a.png)

changedはMutableState(false)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/301e10b7-5071-032c-f7a2-6a4c9d474ac7.png)

ここで recomposerに`snapshotInvalidations`のリストにstateが保存された。

---

runRecomposeAndApplyChangesというずっと走っている関数がいて、そこから、recordComposerModificationsLocked()が呼び出される。
そしてこのsnapshotInvalidationsが使われる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/62fadc44-7f45-69b8-6cfe-a61472a47668.png)

```kotlin:Recomposer.kt
    private fun recordComposerModificationsLocked() {
        if (snapshotInvalidations.isNotEmpty()) {
            snapshotInvalidations.fastForEach { changes ->
                knownCompositions.fastForEach { composition ->
                    composition.recordModificationsOf(changes) // ← ここを読んでいく。
                }
            }
            snapshotInvalidations.clear()
            if (deriveStateLocked() != null) {
                error("called outside of runRecomposeAndApplyChanges")
            }
        }
    }
```

snapshotInvalidationsには依然として保存されている。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/3167e648-3779-4dd0-c4d1-ae4802d8ec03.png)
knownCompositionsはandroidx.compose.runtime.CompositionImplのインスタンスで、一つだけあるみたいでcomposeInitialというのを呼ぶとできるものみたい。

---

```kotlin:Composition.kt
    @Suppress("UNCHECKED_CAST")
    override fun recordModificationsOf(values: Set<Any>) {
        while (true) {
            val old = pendingModifications.get()
            val new: Any = when (old) { // ← oldはnullになっているので、value = MutableState(false)が使われる
                null, PendingApplyNoModifications -> values
                is Set<*> -> arrayOf(old, values)
                is Array<*> -> (old as Array<Set<Any>>) + values
                else -> error("corrupt pendingModifications: $pendingModifications")
            }
            if (pendingModifications.compareAndSet(old, new)) {
                // ここでpendingModificationsはAtomicBooleanでnullが入っている。
                // newはMutableState(false)なので、ここでsetが成功しtrueが返る
                if (old == null) {
                    synchronized(lock) {
                        drainPendingModificationsLocked() // ここが呼ばれる
                    }
                }
                break
            }
        }
    }
```

引数のvalueはHashSetでMutableState(false)が入っている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/b1617cc8-c534-69e0-861e-c9e17fc48513.png)

---


```kotlin:Composition.kt
    private fun drainPendingModificationsLocked() {
        // ここのgetAndSetが成功する
        when (val toRecord = pendingModifications.getAndSet(null)) { // toRecordはHashSetでMutableState(false)が入っている。

            PendingApplyNoModifications -> {
                // No work to do
            }
            //　以下がヒットする。
            is Set<*> -> addPendingInvalidationsLocked(toRecord as Set<Any>)
            is Array<*> -> for (changed in toRecord as Array<Set<Any>>) {
                addPendingInvalidationsLocked(changed)
            }
            null -> error(
                "calling recordModificationsOf and applyChanges concurrently is not supported"
            )
            else -> error(
                "corrupt pendingModifications drain: $pendingModifications"
            )
        }
    }
```

toRecordはHashSetでMutableState(false)が入っている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/632cfeec-bda0-a4ac-2160-fe1d2854eb81.png)

---


valuesはHashSetでMutableState(false)が入っている。

```kotlin:Composition.kt
    private fun addPendingInvalidationsLocked(values: Set<Any>) {
        var invalidated: HashSet<RecomposeScopeImpl>? = null

        fun invalidate(value: Any) {
            observations.forEachScopeOf(value) { scope ->
                if (
                    !observationsProcessed.remove(value, scope) &&
                    scope.invalidateForResult(value) != InvalidationResult.IGNORED
                ) {
                    val set = invalidated // invalidateはnullなので、hashsetがinvalidatedに入る。
                        ?: HashSet<RecomposeScopeImpl>().also {
                            invalidated = it
                        }
                    set.add(scope) // scopeにはなんとContentの関数が入っている！！！
                }
            }
        }
        for (value in values) {
            // 以下はfalseになる
            if (value is RecomposeScopeImpl) {
                value.invalidateForResult(null)
            } else {
                invalidate(value) // ここが呼ばれる。
                derivedStates.forEachScopeOf(value) {
                    invalidate(it)
                }
            }
        }
        invalidated?.let {
            observations.removeValueIf { scope -> scope in it }
        }
    }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/09b31db0-c941-4d15-bc4d-5334f64cc884.png)

---

value = MutableState(value=false)

```kotlin:IdentityScopeMap.kt
    inline fun forEachScopeOf(value: Any, block: (scope: T) -> Unit) {
        val index = find(value)
        if (index >= 0) {
            scopeSetAt(index).forEach(block)
        }
    }
```

---

ここはよくわからないが、key-valueを持っていて、keyにstate、valueにスコープがあり、、うまくhashcodeを使って、Scopeを取り出しているっぽい？


```kotlin:IdentityScopeMap.kt

    /**
     * Returns the index into [valueOrder] of the found [value] of the
     * value, or the negative index - 1 of the position in which it would be if it were found.
     */
    private fun find(value: Any?): Int {
        val valueIdentity = identityHashCode(value)
        var low = 0
        var high = size - 1

        while (low <= high) {
            val mid = (low + high).ushr(1)
            val midValue = valueAt(mid)
            val midValHash = identityHashCode(midValue)
            val comparison = midValHash - valueIdentity
            when {
                comparison < 0 -> low = mid + 1
                comparison > 0 -> high = mid - 1
                value === midValue -> return mid
                else -> return findExactIndex(mid, value, valueIdentity)
            }
        }
        return -(low + 1)
    }
```

0<=0で、value === midValueなので、midを返す。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/71122e26-7b9a-87c9-1a62-06f3a105bbb9.png)

ここで、Scopeを発見。これがContent関数内のラムダになっている

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/e22a47ff-0835-06b3-9aaf-6a8385695713.png)



---


```kotlin:Composition.kt
        fun invalidate(value: Any) {
            observations.forEachScopeOf(value) { scope ->
                if (
                    !observationsProcessed.remove(value, scope) &&
                    // ↓ここに注目！！
                    scope.invalidateForResult(value) != InvalidationResult.IGNORED 
                ) {
                    val set = invalidated // invalidateはnullなので、hashsetがinvalidatedに入る。
                        ?: HashSet<RecomposeScopeImpl>().also {
                            invalidated = it
                        }
                    set.add(scope)
                }
            }
        }
```


---

```kotlin
    /**
     * Invalidate the group which will cause [composition] to request this scope be recomposed,
     * and an [InvalidationResult] will be returned.
     */
    fun invalidateForResult(value: Any?): InvalidationResult =
        composition?.invalidate(this, value) ?: InvalidationResult.IGNORED
```


```kotlin:Composition.kt
    fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult {
        if (scope.defaultsInScope) {
            scope.defaultsInvalid = true
        }
        val anchor = scope.anchor
        if (anchor == null || !slotTable.ownsAnchor(anchor) || !anchor.valid)
            return InvalidationResult.IGNORED // The scope has not yet entered the composition
        val location = anchor.toIndexFor(slotTable)
        if (location < 0)
            return InvalidationResult.IGNORED // The scope was removed from the composition
        if (isComposing && composer.tryImminentInvalidation(scope, instance)) {
            // The invalidation was redirected to the composer.
            return InvalidationResult.IMMINENT
        }

        // invalidations[scope] containing an explicit null means it was invalidated
        // unconditionally.
        if (instance == null) {
            invalidations[scope] = null
        } else {
            invalidations.addValue(scope, instance)
        }

        parent.invalidate(this)
        return if (isComposing) InvalidationResult.DEFERRED else InvalidationResult.SCHEDULED
    }
```

invalidationsにscopeとMutableStateを入れる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/64c2d290-9e5f-3450-41ea-3052e8d32610.png)

---

```kotlin:Recomposer.kt
    internal override fun invalidate(composition: ControlledComposition) {
        synchronized(stateLock) {
            if (composition !in compositionInvalidations) {
                compositionInvalidations += composition
                deriveStateLocked()
            } else null
        }?.resume(Unit)
    }
```

compositionInvalidationsに追加。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/bc1e4b53-5804-2300-e169-e47fe323a2ef.png)

---

戻って ` if (isComposing) InvalidationResult.DEFERRED else InvalidationResult.SCHEDULED`がfalseなので`InvalidationResult.SCHEDULED`を返す


```kotlin:Composition.kt
    fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult {
        if (scope.defaultsInScope) {
            scope.defaultsInvalid = true
        }
        val anchor = scope.anchor
        if (anchor == null || !slotTable.ownsAnchor(anchor) || !anchor.valid)
            return InvalidationResult.IGNORED // The scope has not yet entered the composition
        val location = anchor.toIndexFor(slotTable)
        if (location < 0)
            return InvalidationResult.IGNORED // The scope was removed from the composition
        if (isComposing && composer.tryImminentInvalidation(scope, instance)) {
            // The invalidation was redirected to the composer.
            return InvalidationResult.IMMINENT
        }

        // invalidations[scope] containing an explicit null means it was invalidated
        // unconditionally.
        if (instance == null) {
            invalidations[scope] = null
        } else {
            invalidations.addValue(scope, instance)
        }

        parent.invalidate(this)
        return if (isComposing) InvalidationResult.DEFERRED else InvalidationResult.SCHEDULED
    }
```

---

このあと、 
Composition.invalidationsに入ったscopeとMutableStateを追ってみる。
runRecomposeAndApplyChangesからperformRecomposeが呼ばれ、Recomposeが呼ばれ、保存されたinvalidationsが取得される。

```kotlin:
    suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->
        val toRecompose = mutableListOf<ControlledComposition>()
        val toApply = mutableListOf<ControlledComposition>()
...
                    synchronized(stateLock) {
                        recordComposerModificationsLocked()
                        // compositionInvalidationsをtoRecomposeに入れる
                        compositionInvalidations.fastForEach { toRecompose += it }
                        compositionInvalidations.clear()
                    }

                    // Perform recomposition for any invalidated composers
                    val modifiedValues = IdentityArraySet<Any>()
                    val alreadyComposed = IdentityArraySet<ControlledComposition>()
                    while (toRecompose.isNotEmpty()) {
                        try {
                            toRecompose.fastForEach { composition ->
                                alreadyComposed.add(composition)
                                // **ここでRecompose**
                                performRecompose(composition, modifiedValues)?.let {
                                    // ** toApplyに適応するものを入れる **
                                    toApply += it
                                }
                            }
                        } finally {
                            toRecompose.clear()
                        }
...
                    if (toApply.isNotEmpty()) {
                        changeCount++

                        // Perform apply changes
                        try {
                            // **ここでtoApplyに入った適応させるものをEachで、applyChangesを呼ぶ。**
                            toApply.fastForEach { composition -> 
                                composition.applyChanges()
                            }
                        } finally {
                            toApply.clear()
                        }
                    }

```

---

```kotlin:Composition.kt
    override fun recompose(): Boolean = synchronized(lock) {
        drainPendingModificationsForCompositionLocked()
        composer.recompose(takeInvalidations()).also { shouldDrain ->
            // Apply would normally do this for us; do it now if apply shouldn't happen.
            if (!shouldDrain) drainPendingModificationsLocked()
        }
    }
```

---

```kotlin
    private fun performRecompose(
        composition: ControlledComposition,
        modifiedValues: IdentityArraySet<Any>?
    ): ControlledComposition? {
        if (composition.isComposing || composition.isDisposed) return null
        return if (
            composing(composition, modifiedValues) {
                if (modifiedValues?.isNotEmpty() == true) {
                    // Record write performed by a previous composition as if they happened during
                    // composition.
                    composition.prepareCompose {
                        modifiedValues.forEach { composition.recordWriteOf(it) }
                    }
                }
                composition.recompose()
            }
        ) composition else null
    }
```

---


invalidationsRequestedにはkeyとして変更時に実行するスコープ、valueにMutableState(value=false)が入っている。


```kotlin:Composer.kt
    internal fun recompose(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>
    ): Boolean {
        runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
        // even if invalidationsRequested is empty we still need to recompose if the Composer has
        // some invalidations scheduled already. it can happen when during some parent composition
        // there were a change for a state which was used by the child composition. such changes
        // will be tracked and added into `invalidations` list.
        if (invalidationsRequested.isNotEmpty() || invalidations.isNotEmpty()) {
            doCompose(invalidationsRequested, null) // ← ここを読んでいく
            return changes.isNotEmpty()
        }
        return false
    }
```

そして、やっと、doCompose()にたどり着きました。

---

invalidationsRequestedにはkeyとして変更時に実行するスコープ、valueにMutableState(value=false)が入っています。


```kotlin:Composer.kt
    private fun doCompose(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
        content: (@Composable () -> Unit)?
    ) {
        runtimeCheck(!isComposing) { "Reentrant composition is not supported" }
        trace("Compose:recompose") {
            snapshot = currentSnapshot()
            invalidationsRequested.forEach { scope, set ->
                val location = scope.anchor?.location ?: return
                invalidations.add(Invalidation(scope, location, set))
            }
            invalidations.sortBy { it.location }
            nodeIndex = 0
            var complete = false
            isComposing = true
            try {
                startRoot()
                // Ignore reads of derivedStatOf recalculations
                observeDerivedStateRecalculations(
                    start = {
                        childrenComposing++
                    },
                    done = {
                        childrenComposing--
                    },
                ) {
                    if (content != null) {
                        startGroup(invocationKey, invocation)

                        invokeComposable(this, content)
                        endGroup()
                    } else {
                        skipCurrentGroup()
                    }
                }
                endRoot()
                complete = true
            } finally {
                isComposing = false
                invalidations.clear()
                providerUpdates.clear()
                if (!complete) abortRoot()
            }
        }
    }
```