LoginSignup
5
5

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-08-30

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

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

@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) が呼ばれる。

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()を読んでいく

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

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


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

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)

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!!!
Snapshot.kt
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
    snapshot.writeObserver?.invoke(state)
}

commitPending = falseで呼ばれる

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

結果changes=trueになる。

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

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


observerはこのラムダ。

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

changedはMutableState(false)

image.png

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


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

image.png

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
knownCompositionsはandroidx.compose.runtime.CompositionImplのインスタンスで、一つだけあるみたいでcomposeInitialというのを呼ぶとできるものみたい。


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


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


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

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


value = MutableState(value=false)

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を取り出しているっぽい?

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

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

image.png


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

    /**
     * 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
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


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

compositionInvalidationsに追加。

image.png


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

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が取得される。

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


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

    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)が入っている。

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)が入っています。

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()
            }
        }
    }
5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5