2
2

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のdoCompose()開始からアプリ側のComposable関数の呼び出しに至るまでのコードリーディング

Last updated at Posted at 2021-09-12

https://qiita.com/takahirom/items/d2a89560f8ff2065a7c0
の続きのstateが書き換わってからの動きを追っています。

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

自分用のコードリーディイングメモ記事で、もっとわかりやすいのを後で出すと思います。

以下runRecomposeAndApplyChanges()内処理のまとめ。

以下の中で3:の部分を読んでいきます。3の途中までになります。

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

ComposerImpl.doCompose() 始めからstartRoot()まで

まずdoComposeを見ていく。

ComposerImpl.doCompose()

// invalidationsRequestedとcontentの引数に取る
// invalidationsRequestedはステップ1によって作られたCompositeImpl.invalidationsを使って作られている。
// contentはnullになっている。今回は差分を見ていっているので、これは最初のときにしか使わないと思われる。
    private fun doCompose(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
        content: (@Composable () -> Unit)?
    ) {
        runtimeCheck(!isComposing) { "Reentrant composition is not supported" }
        trace("Compose:recompose") {
// 現在のsnapshotを取得してcomposerに保存する
            snapshot = currentSnapshot()
// invalidationsRequestedをComposerImpl.invalidationsに入れていく。
            invalidationsRequested.forEach { scope, set ->
                val location = scope.anchor?.location ?: return
                invalidations.add(Invalidation(scope, location, set))
            }
// locationでソートする
// locationのコメント: The index of the group in the slot table being invalidated.
            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()
            }
        }
    }

invalidationsRequestedをComposerImpl.invalidationsに入れていくが実際どのような情報がはいるか?

Scopeには実際に呼び出すためのブロックが入っている。

image.png

locationは4

image.png
setにはArraySetでMutableStateが入っている。
image.png

startRoot() 始めからstartGroup()まで

    @OptIn(InternalComposeApi::class)
    private fun startRoot() {
// slotTableのReaderを開く。つまり、SlotTableを読み込みを開始
// これをフィールドに入れる。
        reader = slotTable.openReader()
// startGroup
        startGroup(rootKey)

        // parent reference management
        parentContext.startComposing()
        parentProvider = parentContext.getCompositionLocalScope()
        providersInvalidStack.push(providersInvalid.asInt())
        providersInvalid = changed(parentProvider)
        if (!collectParameterInformation) {
            collectParameterInformation = parentContext.collectingParameterInformation
        }
        resolveCompositionLocal(LocalInspectionTables, parentProvider)?.let {
            it.add(slotTable)
            parentContext.recordInspectionTable(it)
        }
        startGroup(parentContext.compoundHashKey)
    }

startGroup() 始めからstartReaderGroup()まで

ComposerImpl.startGroup()
    private fun startGroup(key: Int) = start(key, null, false, null)

長い関数来ました。。
ここではついにSlotTableとメモリ内の状況の比較が動き始めます
inserting = falseなので、 if文の中は読まなくて良いです。

ComposerImpl.start()
    private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) {
...
        // Check for the insert fast path. If we are already inserting (creating nodes) then
        // there is no need to track insert, deletes and moves with a pending changes object.
        if (inserting) { // ここはfalseなので。このif文の中は今回は読まない。
...
            return
        }

// **ここはtrueになる**
        if (pending == null) {
            val slotKey = reader.groupKey
// ここもtrueになる
// **ここで今のSlotTableの状況とstartRoot()で渡されてきたkeyが同じかを比較する!**
            if (slotKey == key && objectKey == reader.groupObjectKey) {
                // The group is the same as what was generated last time.
// startReaderGroupが呼び出される。
                startReaderGroup(isNode, data)
            } else {
...
            }
        }

        val pending = pending
        var newPending: Pending? = null
        if (pending != null) {
// if文に入らないので、今回は省略
...
        }

        enterGroup(isNode, newPending)
    }

今のSlotTableの状況はこちら。

reader.groupKeyは今100になっており、currentGroupは0になっている。
デバッグで情報を確かめると以下の通り。

reader.run {
    "SlotReader(current = $currentGroup end=$currentEnd size = $size)"
}

SlotReader(current = 0 end=16 size = 16)

Group(0) key=100, nodes=2, size=16, slots=[0: {}] **← このグループ、つまりルートのグループを見るようになっている**
 Group(1) key=1000, nodes=2, size=15
  Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider)
   Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@1502095, androidx.compose.runtime.internal.ComposableLambdaImpl@14975aa]
    Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@99d159b]
     Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@89759032]
     Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@89759032, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>]
     Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp
      Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@6eff07f]
     Group(9) key=-337788167, nodes=1, size=4
      Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@ba14276]
       Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
        Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1]
     Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@32d7077]
      Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
       Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2]

startReaderGroup()

reader.startGroup()を呼ぶと、readerのcurrentGroupなどの位置が変更になる。

ComposerImpl
    private fun startReaderGroup(isNode: Boolean, data: Any?) {
        if (isNode) { // falseになる
            reader.startNode()
        } else {
            if (data != null && reader.groupAux !== data) { // falseになる
                recordSlotTableOperation { _, slots, _ ->
                    slots.updateAux(data)
                }
            }
            reader.startGroup() // ここは呼ばれる。
        }
    }
    fun startGroup() {
        if (emptyCount <= 0) {
            require(groups.parentAnchor(currentGroup) == parent) { "Invalid slot table detected" }
            parent = currentGroup
            currentEnd = currentGroup + groups.groupSize(currentGroup)
            val current = currentGroup++
            currentSlot = groups.slotAnchor(current)
            currentSlotEnd = if (current >= groupsSize - 1)
                slotsSize else
                groups.dataAnchor(current + 1)
        }
    }

reader.startGroup()が呼ばれたあとのreaderの状況は以下のように変化する

image.png
SlotReader(current = 1 end=16 size = 16)

Group(0) key=100, nodes=2, size=16, slots=[0: {}]
 Group(1) key=1000, nodes=2, size=15 **← このグループを見るようになっている**
  Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider)
   Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@1502095, androidx.compose.runtime.internal.ComposableLambdaImpl@14975aa]
    Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@99d159b]
     Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@89759032]
     Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@89759032, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>]
     Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp
      Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@6eff07f]
     Group(9) key=-337788167, nodes=1, size=4
      Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@ba14276]
       Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
        Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1]
     Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@32d7077]
      Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
       Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2]

ここで呼び出し元に戻る

startGroup() startReaderGroup()の終わりからenterGroup()まで

```kotlin:ComposerImpl.start()
    private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) {
...
        // Check for the insert fast path. If we are already inserting (creating nodes) then
        // there is no need to track insert, deletes and moves with a pending changes object.
        if (inserting) { // ここはfalseなので。このif文の中は今回は読まない。
...
            return
        }

// **ここはtrueになる**
        if (pending == null) {
            val slotKey = reader.groupKey
// ここもtrueになる
// **ここで今のSlotTableの状況とstartRoot()で渡されてきたkeyが同じかを比較する!**
            if (slotKey == key && objectKey == reader.groupObjectKey) {
                // The group is the same as what was generated last time.
// startReaderGroupが呼び出される。
                startReaderGroup(isNode, data)
            } else {
...
            }
        }

        val pending = pending
        var newPending: Pending? = null
        if (pending != null) {
// if文に入らないので、今回は省略
...
        }

        enterGroup(isNode, newPending)
    }

enterGroup

SlotReader側のrootが終わったという情報更新は終わっているのでそれをComposer側にも書き込む。

ComposerImpl
    private fun enterGroup(isNode: Boolean, newPending: Pending?) {
        // When entering a group all the information about the parent should be saved, to be
        // restored when end() is called, and all the tracking counters set to initial state for the
        // group.
        pendingStack.push(pending)
        this.pending = newPending
        this.nodeIndexStack.push(nodeIndex)
        if (isNode) nodeIndex = 0
        this.groupNodeCountStack.push(groupNodeCount)
        groupNodeCount = 0
    }

ここで、startRoot()まで処理が戻る。

startRoot()、startGroup(rootKey)の終わりから最後まで。

    @OptIn(InternalComposeApi::class)
    private fun startRoot() {
// slotTableのReaderを開く。つまり、SlotTableを読み込みを開始
// これをフィールドに入れる。
        reader = slotTable.openReader()
// startGroup
        startGroup(rootKey)

        // parent reference management
// あんまり今回はparent reference managementについては考えなくて良さそう。
// parentContext.startComposing()の中身は今回は空になっている。
        parentContext.startComposing()
// EmptyCompositionLocalMapを返してくるのみ
        parentProvider = parentContext.getCompositionLocalScope()
        providersInvalidStack.push(providersInvalid.asInt())
// 以下providersInvalidはfalseになる
        providersInvalid = changed(parentProvider)
        if (!collectParameterInformation) {
            collectParameterInformation = parentContext.collectingParameterInformation
        }
// 以下も今回はあんまり考えなくて良さそう
        resolveCompositionLocal(LocalInspectionTables, parentProvider)?.let {
            it.add(slotTable)
            parentContext.recordInspectionTable(it)
        }
        startGroup(parentContext.compoundHashKey)
    }

startGroupを呼ぶ。
このときの引数となるcompoundHashKeyはRecomposerCompoundHashKeyになっており、1000という固定値。

    internal override val compoundHashKey: Int
        get() = RecomposerCompoundHashKey

image.png

startGroupを呼び出すと先程の繰り返しを行う。

この結果、SlotReaderは以下の状態になる。currentが2になり、少し進む。

SlotReader(current = 2 end=16 size = 16)

Group(0) key=100, nodes=2, size=16, slots=[0: {}]
 Group(1) key=1000, nodes=2, size=15
  Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider) **← このグループを見るようになっている**
   Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@1502095, androidx.compose.runtime.internal.ComposableLambdaImpl@14975aa]
    Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@99d159b]
     Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@89759032]
     Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@89759032, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>]
     Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp
      Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@6eff07f]
     Group(9) key=-337788167, nodes=1, size=4
      Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@ba14276]
       Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
        Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1]
     Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@32d7077]
      Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
       Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2]

ComposerImpl.doCompose() startRoot()の終わりからobserveDerivedStateRecalculations()まで

ComposerImpl.doCompose()

// invalidationsRequestedとcontentの引数に取る
// invalidationsRequestedはステップ1によって作られたCompositeImpl.invalidationsを使って作られている。
// contentはnullになっている。今回は差分を見ていっているので、これは最初のときにしか使わないと思われる。
    private fun doCompose(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
        content: (@Composable () -> Unit)?
    ) {
        runtimeCheck(!isComposing) { "Reentrant composition is not supported" }
        trace("Compose:recompose") {
// 現在のsnapshotを取得してcomposerに保存する
            snapshot = currentSnapshot()
// invalidationsRequestedをComposerImpl.invalidationsに入れていく。
            invalidationsRequested.forEach { scope, set ->
                val location = scope.anchor?.location ?: return
                invalidations.add(Invalidation(scope, location, set))
            }
// locationでソートする
// locationのコメント: The index of the group in the slot table being invalidated.
            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()
            }
        }
    }

SnapshotState.kt observeDerivedStateRecalculations()

internal fun <R> observeDerivedStateRecalculations(
    start: (derivedState: State<*>) -> Unit,
    done: (derivedState: State<*>) -> Unit,
    block: () -> R
) {
    val previous = derivedStateObservers.get()
    try {
        derivedStateObservers.set(
            (derivedStateObservers.get() ?: persistentListOf()).add(
                start to done
            )
        )
        block()
    } finally {
        derivedStateObservers.set(previous)
    }
}

今回は特にderivedStateObserversにはまともなデータは入っていなそう。

image.png

ただ、このderivedStateObservers.set()を行ったあとにはstartとdoneが保存される。
そして終わったらpreviousとしてまた戻される。

image.png

ComposerImpl.doCompose() observeDerivedStateRecalculations()のブロック

ComposerImpl.doCompose()

// invalidationsRequestedとcontentの引数に取る
// invalidationsRequestedはステップ1によって作られたCompositeImpl.invalidationsを使って作られている。
// contentはnullになっている。今回は差分を見ていっているので、これは最初のときにしか使わないと思われる。
    private fun doCompose(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
        content: (@Composable () -> Unit)?
    ) {
        runtimeCheck(!isComposing) { "Reentrant composition is not supported" }
        trace("Compose:recompose") {
// 現在のsnapshotを取得してcomposerに保存する
            snapshot = currentSnapshot()
// invalidationsRequestedをComposerImpl.invalidationsに入れていく。
            invalidationsRequested.forEach { scope, set ->
                val location = scope.anchor?.location ?: return
                invalidations.add(Invalidation(scope, location, set))
            }
// locationでソートする
// locationのコメント: The index of the group in the slot table being invalidated.
            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) { // ここでcontentがnullなので入らない!!
                        startGroup(invocationKey, invocation)

                        invokeComposable(this, content)
                        endGroup()
                    } else {
// skipCurrentGroup()が呼ばれる
                        skipCurrentGroup()
                    }
                }
                endRoot()
                complete = true
            } finally {
                isComposing = false
                invalidations.clear()
                providerUpdates.clear()
                if (!complete) abortRoot()
            }
        }
    }

skipCurrentGroup()からrecomposeToGroupEnd()まで

ComposerImpl
    override fun skipCurrentGroup() {
// ここはfalseになる
// invalidationsには先程入れた変更場所情報などが入っている。
        if (invalidations.isEmpty()) {
            skipGroup()
        } else {
            val reader = reader
            val key = reader.groupKey
            val dataKey = reader.groupObjectKey
            val aux = reader.groupAux
// compoundKeyHashはsaved stateのために使うみたいなので、ここでは読まない。
            updateCompoundKeyWhenWeEnterGroup(key, dataKey, aux)
// 以下を呼ぶとreaderの現在の位置が変わる。
            startReaderGroup(reader.isNode, null)
            recomposeToGroupEnd()
            reader.endGroup()
            updateCompoundKeyWhenWeExitGroup(key, dataKey, aux)
        }
    }

SlotReader(current = 3 end=16 size = 16)

Group(0) key=100, nodes=2, size=16, slots=[0: {}]
 Group(1) key=1000, nodes=2, size=15
  Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider)
 **↓ このグループを見るようになっている**
   Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@1502095, androidx.compose.runtime.internal.ComposableLambdaImpl@14975aa] 
    Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@99d159b]
     Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@89759032]
     Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@89759032, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>]
     Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp
      Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@6eff07f]
     Group(9) key=-337788167, nodes=1, size=4
      Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@ba14276]
       Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
        Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1]
     Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@32d7077]
      Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
       Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2]

recomposeToGroupEnd()、 始めからrecordUpsAndDowns()まで

    /**
     * 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].
     * 今のparent groupの無効になったchild groupをすべてrecomposeする。
     * これはグループ開始後に呼ばれるべき。
     * 無効になったchild groupがある場合にskipReaderToGroupEnd()の代わりに呼ばれることを意図している。
     * もし無効になったchildがいなければ、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

        // invalidationsは1つしかないので、自動的にその1つが選ばれる。 (これまで何度か出てきたinvalidations)
        var firstInRange = invalidations.firstInRange(reader.currentGroup, end)
        while (firstInRange != null) {
            val location = firstInRange.location
// ここでinvalidationsは消される!
            invalidations.removeLocation(location)

// 以下はtrueになる
            if (firstInRange.isInvalid()) {
                recomposed = true

// reader.reposition(location)によってreaderの見ている位置が移動!!
                reader.reposition(location)
// 移動した今の位置を取得
                val newGroup = reader.currentGroup
                // Record the changes to the applier location
                recordUpsAndDowns(oldGroup, newGroup, parent)
                oldGroup = newGroup

                // Calculate the node index (the distance index in the node this groups nodes are
                // located in the parent node).
                nodeIndex = nodeIndexOf(
                    location,
                    newGroup,
                    parent,
                    recomposeIndex
                )

                // Calculate the compound hash code (a semi-unique code for every group in the
                // composition used to restore saved state).
                compoundKeyHash = compoundKeyOf(
                    reader.parent(newGroup),
                    parent,
                    recomposeCompoundKey
                )

                firstInRange.scope.compose(this)

                // Restore the parent of the reader to the previous parent
                reader.restoreParent(parent)
            } else {
                // If the invalidation is not used restore the reads that were removed when the
                // the invalidation was recorded. This happens, for example, when on of a derived
                // state's dependencies changed but the derived state itself was not changed.
                invalidateStack.push(firstInRange.scope)
                firstInRange.scope.rereadTrackedInstances()
                invalidateStack.pop()
            }

            // Using slots.current here ensures composition always walks forward even if a component
            // before the current composition is invalidated when performing this composition. Any
            // such components will be considered invalid for the next composition. Skipping them
            // prevents potential infinite recomposes at the cost of potentially missing a compose
            // as well as simplifies the apply as it always modifies the slot table in a forward
            // direction.
            firstInRange = invalidations.firstInRange(reader.currentGroup, end)
        }

        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
    }

reader.reposition(location)後
SlotReader(current = 4 end=16 size = 16)

Group(0) key=100, nodes=2, size=16, slots=[0: {}]
 Group(1) key=1000, nodes=2, size=15
  Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider)
   Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@1502095, androidx.compose.runtime.internal.ComposableLambdaImpl@14975aa] 
 **↓ このグループを見るようになっている**
    Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@99d159b]
     Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@89759032]
     Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@89759032, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>]
     Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp
      Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@6eff07f]
     Group(9) key=-337788167, nodes=1, size=4
      Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@ba14276]
       Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
        Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1]
     Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@32d7077]
      Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
       Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2]

recordUpsAndDowns()とdoRecordDownsFor() (今回は何もしないので飛ばしてOK)

ComposerImpl
    /**
     * Records the operations necessary to move the applier the node affected by the previous
     * group to the new group.
     */
    private fun recordUpsAndDowns(oldGroup: Int, newGroup: Int, commonRoot: Int) {
        val reader = reader
        val nearestCommonRoot = reader.nearestCommonRootOf(
            oldGroup,
            newGroup,
            commonRoot
        )

        // Record ups for the nodes between oldGroup and nearestCommonRoot
        var current = oldGroup
        while (current > 0 && current != nearestCommonRoot) { // このif文の中には入らない。
            if (reader.isNode(current)) recordUp()
            current = reader.parent(current)
        }

        // Record downs from nearestCommonRoot to newGroup
        doRecordDownsFor(newGroup, nearestCommonRoot)
    }

以下で再帰処理をしていて、nearestCommonRoot = 2でgroup = 4なので、もう一度doRecordDownsFor()が呼ばれなおされ、次はnearestCommonRoot = 2でgroup = 3で呼ばれ、最後はnearestCommonRoot = 2でgroup = 2で呼ばれて、returnするが、全てisNode()がfalseになるのでなにもしない。

    private fun doRecordDownsFor(group: Int, nearestCommonRoot: Int) {
        if (group > 0 && group != nearestCommonRoot) {
            doRecordDownsFor(reader.parent(group), nearestCommonRoot)
            if (reader.isNode(group)) recordDown(reader.nodeAt(group))
        }

recomposeToGroupEnd()、 recordUpsAndDowns()の終わりからfirstInRange.scope.compose(this)まで

    /**
     * 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].
     * 今のparent groupの無効になったchild groupをすべてrecomposeする。
     * これはグループ開始後に呼ばれるべき。
     * 無効になったchild groupがある場合にskipReaderToGroupEnd()の代わりに呼ばれることを意図している。
     * もし無効になったchildがいなければ、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

        // invalidationsは1つしかないので、自動的にその1つが選ばれる。 (これまで何度か出てきたinvalidations)
        var firstInRange = invalidations.firstInRange(reader.currentGroup, end)
        while (firstInRange != null) {
            val location = firstInRange.location
// ここでinvalidationsは消される!
            invalidations.removeLocation(location)

// 以下はtrueになる
            if (firstInRange.isInvalid()) {
                recomposed = true

// reader.reposition(location)によってreaderの見ている位置が移動!!
                reader.reposition(location)
// 移動した今の位置を取得
                val newGroup = reader.currentGroup
                // Record the changes to the applier location
                recordUpsAndDowns(oldGroup, newGroup, parent)
                oldGroup = newGroup

                // Calculate the node index (the distance index in the node this groups nodes are
                // located in the parent node).
                // 親のノードの中に位置しているこのグループのノードたちの中での距離のindexを求める
                // これによってComposer側のnodeIndexが更新される。
                nodeIndex = nodeIndexOf(
                    location,
                    newGroup,
                    parent,
                    recomposeIndex
                )

                // Calculate the compound hash code (a semi-unique code for every group in the
                // composition used to restore saved state).
                // savedStateで使う値。
                compoundKeyHash = compoundKeyOf(
                    reader.parent(newGroup),
                    parent,
                    recomposeCompoundKey
                )
// ↓!!!
                firstInRange.scope.compose(this)

                // Restore the parent of the reader to the previous parent
                reader.restoreParent(parent)
            } else {
                // If the invalidation is not used restore the reads that were removed when the
                // the invalidation was recorded. This happens, for example, when on of a derived
                // state's dependencies changed but the derived state itself was not changed.
                invalidateStack.push(firstInRange.scope)
                firstInRange.scope.rereadTrackedInstances()
                invalidateStack.pop()
            }

            // Using slots.current here ensures composition always walks forward even if a component
            // before the current composition is invalidated when performing this composition. Any
            // such components will be considered invalid for the next composition. Skipping them
            // prevents potential infinite recomposes at the cost of potentially missing a compose
            // as well as simplifies the apply as it always modifies the slot table in a forward
            // direction.
            firstInRange = invalidations.firstInRange(reader.currentGroup, end)
        }

        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
    }

nodeIndexOfの計算

image.png

そしてやっと、アプリ側のComposable関数までたどり着きました。

    /**
     * Restart the scope's composition. It is an error if [block] was not updated. 
     * scopeのcompositionをrestartする。もしblockが更新(代入)されていないとエラーになる。以下はそのエラーになるパターンの説明。
     * The code
     * generated by the compiler ensures that when the recompose scope is used then [block] will
     * be set but it might occur if the compiler is out-of-date (or ahead of the runtime) or
     * incorrect direct calls to [Composer.startRestartGroup] and [Composer.endRestartGroup].
     */
    fun compose(composer: Composer) {
        block?.invoke(composer, 1) ?: error("Invalid restart scope")
    }

ここからお楽しみのアプリ側のRecomposeです。

まとめ

今回は基本的にSlotTableのReaderがrootからcurrentGroupを変えて、アプリ側が強調しながら動いていることがなんとなくわかりました。また変更のところまでジャンプをしていき、最終的にアプリ側のblockを呼び出すところまで生きました。

2
2
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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?