なんどもフィールドに入ったり他へ保存を繰り返すので結構しんどいコードリーディングになります。
以下の書き換えが走ったときの挙動を追います。
@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の書き換えが走る。 (予想。コード未読)
以下は読んでもいいですが、多分大変だと思います。
まず、set(value) が呼ばれる。
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()を読んでいく
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)
}
}
各引数の状態
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
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)
}
}
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!!!
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeObserver?.invoke(state)
}
commitPending = falseで呼ばれる
@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()
}
}
}
結果changes=trueになる。
fun sendApplyNotifications() {
val changes = sync {
currentGlobalSnapshot.get().modified?.isNotEmpty() == true
}
if (changes)
advanceGlobalSnapshot() // ここを読んでいく
}
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
}
observerはこのラムダ。
val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
synchronized(stateLock) {
if (_state.value >= State.Idle) { // ここは trueになる
snapshotInvalidations += changed // ここで `snapshotInvalidations`のリストに保存する!!!!
deriveStateLocked()
} else null
}?.resume(Unit)
}
条件はtrue
changedはMutableState(false)
ここで recomposerにsnapshotInvalidations
のリストにstateが保存された。
runRecomposeAndApplyChangesというずっと走っている関数がいて、そこから、recordComposerModificationsLocked()が呼び出される。
そしてこのsnapshotInvalidationsが使われる。
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には依然として保存されている。
knownCompositionsはandroidx.compose.runtime.CompositionImplのインスタンスで、一つだけあるみたいでcomposeInitialというのを呼ぶとできるものみたい。
@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)が入っている。
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)が入っている。
valuesはHashSetでMutableState(false)が入っている。
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 }
}
}
value = MutableState(value=false)
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を取り出しているっぽい?
/**
* 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を返す。
ここで、Scopeを発見。これがContent関数内のラムダになっている
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
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を入れる。
internal override fun invalidate(composition: ControlledComposition) {
synchronized(stateLock) {
if (composition !in compositionInvalidations) {
compositionInvalidations += composition
deriveStateLocked()
} else null
}?.resume(Unit)
}
compositionInvalidationsに追加。
戻って if (isComposing) InvalidationResult.DEFERRED else InvalidationResult.SCHEDULED
がfalseなのでInvalidationResult.SCHEDULED
を返す
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()
}
}
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)が入っている。
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)が入っています。
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()
}
}
}