2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Jetpack Composeのrecompose後。GlobalSnapshotへの適応のコードリーディング

Last updated at Posted at 2021-09-30

の続きです。

今度まとめた記事を書く予定なので、この記事は読まなくていいです。

前回までで、recomposeの実装を読んでいくことができました。
これからSlotTableに反映していくところ
つまり3から4の間あたりをみていきます。

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の書き換えが走る。 (予想。コード未読)

前回はContentなどの実際のユーザーが定義した関数の呼び出しを見ていきましたが、それが終わった後の動きについて追っていきます。

Recomposer.runRecomposeAndApplyChanges()
└── Recomposer.performRecompose()
    └── CompositionImpl.recompose()
        └── CompositionImpl.doCompose()
            └── CompositionImpl.skipCurrentGroup()
                └── CompositionImpl.recomposeToGroupEnd()
                    ├── RecomposeScopeImpl.compose()
                    │   └── Content() 前回まで読んでいたもの
                    │       └── Node2()
                    ├── *CompositionImpl.recordUpsAndDowns()* ← Now reading
                    └── *SlotReader.skipToGroupEnd()* ← Now reading
    /**
     * Recompose any invalidate child groups of the current parent group. This should be called
     * after the group is started but on or before the first child group. It is intended to be
     * called instead of [skipReaderToGroupEnd] if any child groups are invalid. If no children
     * are invalid it will call [skipReaderToGroupEnd].
     */
    private fun recomposeToGroupEnd() {
        val wasComposing = isComposing
        isComposing = true
        var recomposed = false

        val parent = reader.parent
        val end = parent + reader.groupSize(parent)
        val recomposeIndex = nodeIndex
        val recomposeCompoundKey = compoundKeyHash
        val oldGroupNodeCount = groupNodeCount
        var oldGroup = parent

        var firstInRange = invalidations.firstInRange(reader.currentGroup, end)
        while (firstInRange != null) {
            val location = firstInRange.location

            invalidations.removeLocation(location)

            if (firstInRange.isInvalid()) {
...
                firstInRange.scope.compose(this) // ← ここを前まで呼んだ
...
        }

        if (recomposed) {
            recordUpsAndDowns(oldGroup, parent, parent) // ← ここから
            reader.skipToGroupEnd()
            val parentGroupNodes = updatedNodeCount(parent)
            nodeIndex = recomposeIndex + parentGroupNodes
            groupNodeCount = oldGroupNodeCount + parentGroupNodes
        } else {
            // No recompositions were requested in the range, skip it.
            skipReaderToGroupEnd()
        }
        compoundKeyHash = recomposeCompoundKey

        isComposing = wasComposing
    }

CompositionImpl.recordUpsAndDowns() & reader.skipToGroupEnd()

デバッガーの動きを見ましたが、今回のrecomposeではほぼ何も行われないようでした。

recordという名前がついているので、changesの内容を確認しておきましょう。
image.png

これは以前の記事と変わっていなそうでした。
https://qiita.com/takahirom/items/11e3ed72eb2f83440b12#%E3%81%BE%E3%81%A8%E3%82%81

0 = record { _, slots, _ -> slots.advanceBy(distance) }
10 進める

1 = private val startRootGroup: Change = { _, slots, _ -> slots.ensureStarted(0) }
ensureStartedGroupが始まったので、いろんなWritercurrentGroupなどを更新をかけるもの。で、引数が0なのでrootでやる。

2 = recordSlotTableOperation { _, slots, _ -> slots.ensureStarted(anchor) }
ensureStartedGroupが始まったので、いろんなWritercurrentGroupなどを更新をかけるもの。で、引数がanchorなのでcurrent groupでやる

3 = private val removeCurrentGroupInstance: Change = { _, slots, rememberManager ->
slots.removeCurrentGroup(rememberManager)
}

4 = { _, slots, _ -> slots.endGroup() }

5 = { applier, _, _ -> applier.remove(removeIndex, count) }

なのでさかのぼって次はendRoot()になりました。

    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()
            }
        }
    }
Recomposer.runRecomposeAndApplyChanges()
└── Recomposer.performRecompose()
    └── CompositionImpl.recompose()
        └── CompositionImpl.doCompose()
            ├── CompositionImpl.skipCurrentGroup()
            │   └── CompositionImpl.recomposeToGroupEnd()
            │       ├── RecomposeScopeImpl.compose()
            │       │   └── Content()
            │       │       └── Node2()
            │       ├── CompositionImpl.recordUpsAndDowns()
            │       └── SlotReader.skipToGroupEnd()
            └── *endRoot()* ← Now reading
    /**
     * End the composition. This should be called, and only be called, to end the first group in
     * the composition.
     * compositionの終わり。最初のcompositionが終わった時のみ呼ばれるべき。
     */
    @OptIn(InternalComposeApi::class)
    private fun endRoot() {
// 以下ではほとんど何もしません
        endGroup()
        parentContext.doneComposing()
        endGroup()
// 上記ではほとんど何もしません
        recordEndRoot()
        finalizeCompose()
        reader.close()
    }

recordEndRoot()

Recomposer.runRecomposeAndApplyChanges()
└── Recomposer.performRecompose()
    └── CompositionImpl.recompose()
        └── CompositionImpl.doCompose()
            ├── CompositionImpl.skipCurrentGroup()
            │   └── CompositionImpl.recomposeToGroupEnd()
            │       ├── RecomposeScopeImpl.compose()
            │       │   └── Content()
            │       │       └── Node2()
            │       ├── CompositionImpl.recordUpsAndDowns()
            │       └── SlotReader.skipToGroupEnd()
            └── ComposerImpl.endRoot()
                ├── ComposerImpl.endGroup()
                ├── Recomposer.doneComposing()
                ├── ComposerImpl.endGroup()
                └── *ComposerImpl.recordEndRoot()* ← Now reading

以下でSlotTable上で3移動してからendGroup()を呼ぶという操作をrecordします。

private val endGroupInstance: Change = { _, slots, _ -> slots.endGroup() }

    private fun recordEndRoot() {
        if (startedGroup) {
            recordSlotTableOperation(change = endGroupInstance)
            startedGroup = false
        }
    }

    /**
     * Record a change ensuring, when it is applied, the write matches the current slot in the
     * reader.
     * 
     */
    private fun recordSlotTableOperation(forParent: Boolean = false, change: Change) {
        realizeOperationLocation(forParent)
        record(change)
    }

    private fun realizeOperationLocation(forParent: Boolean = false) {
        val location = if (forParent) reader.parent else reader.currentGroup
        val distance = location - writersReaderDelta
        require(distance >= 0) { "Tried to seek backward" }
// メモ
// forParent = false
// writersReaderDelta = 13
// location = 16
        if (distance > 0) { // distanceは3になる!!!
            record { _, slots, _ -> slots.advanceBy(distance) }
            writersReaderDelta = location
        }
    }

changesが2つ増えます。

image.png

0 = record { _, slots, _ -> slots.advanceBy(distance) }
10 進める

1 = private val startRootGroup: Change = { _, slots, _ -> slots.ensureStarted(0) }
ensureStartedGroupが始まったので、いろんなWritercurrentGroupなどを更新をかけるもの。で、引数が0なのでrootでやる。

2 = recordSlotTableOperation { _, slots, _ -> slots.ensureStarted(anchor) }
ensureStartedGroupが始まったので、いろんなWritercurrentGroupなどを更新をかけるもの。で、引数がanchorなのでcurrent groupでやる

3 = private val removeCurrentGroupInstance: Change = { _, slots, rememberManager ->
slots.removeCurrentGroup(rememberManager)
}

4 = { _, slots, _ -> slots.endGroup() }

5 = { applier, _, _ -> applier.remove(removeIndex, count) }

6 = { _, slots, _ -> slots.advanceBy(distance) }// (distance = 3)
7 = { _, slots, _ -> slots.endGroup() }

endRoot()に戻る

他の finalizeCompose()reader.close() はフィールドに0を入れて初期化したりなどでした。

    /**
     * End the composition. This should be called, and only be called, to end the first group in
     * the composition.
     */
    @OptIn(InternalComposeApi::class)
    private fun endRoot() {
        endGroup()
        parentContext.doneComposing()
        endGroup()
        recordEndRoot()
        finalizeCompose()
        reader.close()
    }

戻っていきます。

途中でComposerImpl.invalidationsを初期化したりします。

                invalidations.clear()
                providerUpdates.clear()

Recomposer.composing()

Recomposer.runRecomposeAndApplyChanges()
└── Recomposer.performRecompose()
    └── *Recomposer.composing()* ← Now reading
        └── CompositionImpl.recompose()
            └── Snapshot.enter(block)
                └── CompositionImpl.doCompose()
                    ├── CompositionImpl.skipCurrentGroup()
                    │   └── CompositionImpl.recomposeToGroupEnd()
                    │       ├── RecomposeScopeImpl.compose()
                    │       │   └── Content()
                    │       │       └── Node2()
                    │       ├── CompositionImpl.recordUpsAndDowns()
                    │       └── SlotReader.skipToGroupEnd()
                    └── ComposerImpl.endRoot()
                        ├── ComposerImpl.endGroup()
                        ├── Recomposer.doneComposing()
                        ├── ComposerImpl.endGroup()
                        └── ComposerImpl.recordEndRoot()
                            └── ComposerImpl.recordSlotTableOperation(forParent = false, change = { _, slots, _ -> slots.endGroup() })
                                └── ComposerImpl.realizeOperationLocation(forParent = false)

ここではSnapshotを取得します。Snapshotはゲームのセーブポイントみたいなものです。
引数でreadObserverとwriteObserverを渡しますが、これでComposition中の(enterを呼んだ中の)MutableStateへの変更を収集します。

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

enterを呼んだあとの変更をapplyAndCheck()でsnapshot.apply()でGlobalSnapshotに変更を適応します。
これをしないとGlobalSnapshotでMutableStateの状態は変わりません。

    private fun applyAndCheck(snapshot: MutableSnapshot) {
        val applyResult = snapshot.apply()
        if (applyResult is SnapshotApplyResult.Failure) {
            error(
                "Unsupported concurrent change during composition. A state object was " +
                    "modified by composition as well as being modified outside composition."
            )
            // TODO(chuckj): Consider lifting this restriction by forcing a recompose
        }
    }

snapshot.apply()については論文へのリンクが登場するなど面白い部分なのですが、あとで見ていくことになります。

そして、runRecomposeAndApplyChanges()に戻ってきます。

*Recomposer.runRecomposeAndApplyChanges()* ← Now reading
└── Recomposer.performRecompose()
    └── Recomposer.composing()
        └── CompositionImpl.recompose()
            └── Snapshot.enter(block)
                └── CompositionImpl.doCompose()
                    ├── CompositionImpl.skipCurrentGroup()
                    │   └── CompositionImpl.recomposeToGroupEnd()
                    │       ├── RecomposeScopeImpl.compose()
                    │       │   └── Content()
                    │       │       └── Node2()
                    │       ├── CompositionImpl.recordUpsAndDowns()
                    │       └── SlotReader.skipToGroupEnd()
                    └── ComposerImpl.endRoot()
                        ├── ComposerImpl.endGroup()
                        ├── Recomposer.doneComposing()
                        ├── ComposerImpl.endGroup()
                        └── ComposerImpl.recordEndRoot()
                            └── ComposerImpl.recordSlotTableOperation(forParent = false, change = { _, slots, _ -> slots.endGroup() })
                                └── ComposerImpl.realizeOperationLocation(forParent = false)

                    val modifiedValues = IdentityArraySet<Any>()
                    val alreadyComposed = IdentityArraySet<ControlledComposition>()
                    while (toRecompose.isNotEmpty()) {
                        try {
                            toRecompose.fastForEach { composition ->
                                alreadyComposed.add(composition)
                                performRecompose(composition, modifiedValues)?.let {
                                    toApply += it // toApplyにchanges = SlotTableへ適応されるべき変更が入っている
                                } // ← ここが行われていた
                            }
                        } finally {
                            toRecompose.clear() // ←ここ !!
                        }

ここで以下3つぐらい前の記事で作っていたtoRecomposeがclearされます。

そして最後に以下のapply 処理が行われるのですが、それは次の記事で。

                    if (toApply.isNotEmpty()) {
                        changeCount++

                        // Perform apply changes
                        try {
                            toApply.fastForEach { composition ->
                                composition.applyChanges()
                            }
                        } finally {
                            toApply.clear()
                        }
                    }

                    synchronized(stateLock) {
                        deriveStateLocked()
                    }
2
3
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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?