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を見ていく。
// 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には実際に呼び出すためのブロックが入っている。
locationは4
setにはArraySetでMutableStateが入っている。
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()まで
private fun startGroup(key: Int) = start(key, null, false, null)
長い関数来ました。。
ここではついにSlotTableとメモリ内の状況の比較が動き始めます
inserting = falseなので、 if文の中は読まなくて良いです。
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などの位置が変更になる。
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の状況は以下のように変化する
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側にも書き込む。
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
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()まで
// 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にはまともなデータは入っていなそう。
ただ、このderivedStateObservers.set()を行ったあとにはstartとdoneが保存される。
そして終わったらpreviousとしてまた戻される。
ComposerImpl.doCompose() observeDerivedStateRecalculations()のブロック
// 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()まで
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)
/**
* 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の計算
そしてやっと、アプリ側の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を呼び出すところまで生きました。