の続きです。
今度まとめた記事を書く予定なので、この記事は読まなくていいです。
前回までで、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の内容を確認しておきましょう。
これは以前の記事と変わっていなそうでした。
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が始まったので、いろんなWriterのcurrentGroupなどを更新をかけるもの。で、引数が0なのでrootでやる。
2 = recordSlotTableOperation { _, slots, _ -> slots.ensureStarted(anchor) }
ensureStartedGroupが始まったので、いろんなWriterのcurrentGroupなどを更新をかけるもの。で、引数が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つ増えます。
0 = record { _, slots, _ -> slots.advanceBy(distance) }
10 進める
1 = private val startRootGroup: Change = { _, slots, _ -> slots.ensureStarted(0) }
ensureStartedGroupが始まったので、いろんなWriterのcurrentGroupなどを更新をかけるもの。で、引数が0なのでrootでやる。
2 = recordSlotTableOperation { _, slots, _ -> slots.ensureStarted(anchor) }
ensureStartedGroupが始まったので、いろんなWriterのcurrentGroupなどを更新をかけるもの。で、引数が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()
}