本記事には間違った内容が含まれている可能性が高いため参考にする程度でお願いします。間違った点を見つけた場合はコメントなどで教えていただけると幸いです。
また一次ソースとして本記事で参考にしているJetpack Compose internals や最新の Jetpack Compose のソースコードを参照することを推奨します。
スナップショットとは
ある瞬間 の 状態 を保持したもの
イラスト: https://simpc.jp/tec/item/sc-239/
でも Snapshot
って Compose のコードを書いていて見たことなくないですか...?
と思いきやめちゃくちゃ当たりまえに使っています。
Snapshot の身近な例を見てみましょう。
mutableStateOf の実装を見る
var isOn by remember {
mutableStateOf(false) // 実装を見る
}
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
SnapshotMutationPolicy や create SnapshotMutableState というものが使われています。
mutableStateOf は Snapshot と密接に関係がある
もし mutableStateOf を使わなかったらどうなるか試してみましょう
mutableStateOf とそっくりな myMutableStateOf を作ります。
fun <T> myMutableStateOf(initial: T): MyMutableState<T> {
return MyMutableState(initial)
}
class MyMutableState<T>(initial: T) {
var value = initial
}
by
が使えるように Delegated property も書いておきます。
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> MyMutableState<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> MyMutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}
var isOn by remember {
myMutableStateOf(false) // 差し替える
}
すると
状態は変化せず何も起きなくなりります
いくらタップしてもスイッチの状態は変わりません。
でも Flow#collectAsState()
とかは mutableStateOf
を使っていないけど状態の変化を検出できている気がする...?
Flow#collectAsState()
の中身を見る
StateFlow#collectAsState()
@Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
内部で produceState
が使われていることが分かる。
produceState
@Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) } // mutableStateOf が使われている
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
内部で mutableStateOf
が使われていることが分かる。
結局 Compose の状態には必ず mutableStateOf
が使われている
パート1のまとめ
- Snapshot とは 「ある瞬間の状態を保持したもの」である
- Snapshot は
mutableStateOf
と密接に関わっており、Compose に組み込まれたシステムである -
mutableStateOf
を使わないと状態の変化を表現することができない
パート2 ✌️ Snapshot の仕組み
Snapshot の仕組み
- ここからは結構深い話になります
- 業務で直接役に立つことはあんまりないかもれしれません
- しかし、Compose の魔法 の秘密に迫る興味深い内容になっていると思います!
完全に理解しきれていない部分も多々あります。ご了承ください
Snapshot がどのように動作するか
Snapshot は @Composable
外でも動きます。
実際にコードで Snapshot の挙動を観察してみましょう。
Snapshot は Compose のアーキテクチャレイヤの最も下位のレベルの Compose Runtime に含まれています。
サンプルコード
@Test
fun sample() {
val state = mutableStateOf("ツバス")
val snapshot = Snapshot.takeSnapshot()
state.value = "ハマチ"
println(state.value)
snapshot.enter {
println(state.value)
}
snapshot.dispose()
}
順に説明します。
val state = mutableStateOf("ツバス")
mutableStateOf
を使って MutableState
を作成します。
初期値はツバスとします。
※ ツバスはブリの20-40cmサイズの関西での呼び名です。
val snapshot = Snapshot.takeSnapshot()
次に Snapshot#takeSnapshot() を呼び出しでスナップショットを取ります。
Snapshot
クラスは普通に Compose のコードを書いている限り見たことがないと思います。
なぜなら Compose ではコンパイラが自動的に Snapshot 周りの処理をしてくれるからです。
state.value = "ハマチ"
println(state.value)
次に状態を "ハマチ"
に書き換えて println で状態を出力すると結果は "ハマチ"
になります。
ここまではいたって普通の挙動です。
snapshot.enter {
println(state.value)
}
では先程取ったスナップショットの enter
を呼び出し、そのスコープ内で状態の値を出力すると結果はどうなるでしょうか?
考えてみてください
snapshot.enter {
println(state.value) // ツバス
}
正解は "ツバス"
になります。わかりましたか?
そうです、あたかもスナップショットを取った時点の状態に戻ったかのような挙動をするのです。
不思議ですよね。
Snapshot システムがどのように作られているか
状態管理に必要なこと
状態は複数のスレッドで同時に読み込みが行われたり、書き込みが行われたりすることがあります。
その場合でも状態の整合性を確保するための仕組みが必要になります。
いくつかのアプローチ方法がありますが、Compose では アクターモデル に似た方法を採用しています。
並行性制御システム
どのように状態の変化が起きたとしても確実に同調することが必要です。
しかしそのためにはいくらかパフォーマンスを犠牲にする必要があります。そのため可能な限り効率の良い方法を取ることが重要です。
並行性制御システムの例として RDBMS のトランザクションがあります。
Compose の場合は RDBMS ほど複雑でなく、ACID 原則のうちDの部分は不要なので持っていません。
ACID 原則
- Atomicity (不可分性)
- Consistency (整合性)
- Isolation (独立性)
- Durability (永続性)
※ Durability は Compose にはない
MVCC
「MVCC=Multiversion concurrency control、多版型同時実行制御」
Compose で使用されている並行性制御システムです。
全ての処理がどの順番で行われたかを記録しており、すべての変更は ID で管理されています。
独立性があるので書き込み中の読み取りや反対に読み取り中の書き込みを行うことができます。
MVCCに は独立性のためにデータが書き込まれるたびにオリジナルの値を上書きする代わりに新しいデータのコピーを作成します。Compose の実装では StateRecord
クラスのLinked listで実現されています。
MVCC
MVCCのもう1つの特徴としてスナップショットがあります。それぞれのスナップショットにはIDがあり Compose の場合は単調増加します。(1,2,3...)
スレッドごとにある瞬間の状態のスナップショットを持っており、そのスナップショット上で操作が行われます(マルチバージョン)。各スレッドで行われた変更はそれらが完了したら各スレッドのスナップショットに伝播されます。
Snapshot のツリー
Snapshot はツリー構造を持っています。任意の数だけ Snapshot をネストすることができます。ルートは GlobalSnapshot と呼ばれ、子ノードはNested[Readonly|Mutable]Snapshot と呼ばれます。
GlobalSnapshot
GlobalSnapshot は Snapshot のツリーのルートで書き込みが可能なスナップショットです。
Compose の初期化時に GlobalSnapshot が作成されます。
スナップショットのネスト
ネストされたスナップショットは親と独立しているので、親をアクティブに保ちながら dispose することができます。
Compose では Subcomposition で使用されていて、具体的には BoxWithConstraints
LazyColumn
SubcomposeLayout
などで使用されています。ネストされたスナップショットに変更が加えられた場合は親に伝播されます。
状態の読み込みと書き込みの購読
Compose では状態の変化に反応して Recompose をする必要があります。これを実現するためにスナップショットでは状態の読み込みと書き込みを購読できる仕組みが用意されています。
身近な例として Recomposer の他に snapshotFlow
がこの仕組みを使用しています。
読み込みの購読
val snapshot = Snapshot.takeSnapshot(
readObserver = {
println("readObserver: $it")
}
)
readObserver
を使用します。
書き込みの購読
val snapshot = Snapshot.takeMutableSnapshot(
writeObserver = {
println("writeObserver: $it")
}
)
writeObserver
を使用します。readObserver
と違い MutableSnapshot
でしか使えないので注意してください。
MutableSnapshot
Snapshot.takeSnapshot
は ReadonlySnapshot
を返すので書き込みを行うことができません。
Snapshot.takeMutableSnapshot
で書き込み可能なスナップショットを取ることができます。(MutableSnapshot)
書き込みを行ったあと、MutableSnapshot#apply()
を呼びだすことで変更が反映されます。
StateStateRecord
StateRecord
はある状態のある時点での値を保持しているクラスです。
abstract class StateRecord {
internal var snapshotId: Int = currentSnapshot().id
internal var next: StateRecord? = null
abstract fun assign(value: StateRecord)
abstract fun create(): StateRecord
}
snapshotId
StateRecord は 現在の Snapshotに対して作成されます。
snapshotId
には StateRecord が作成された時点のスナップショットIDがセットされています。
next
StateRecord は Linked list で接続されています。next は次の要素を指します。
StateObject
StateObject
は StateRecord の Linked list を管理しています。
mutableStateOf で返される MutableState
の実体である SnapshotMutableStateImpl
は StateObject
を実装しています。
状態が読み込まれるたびに、StateRecord の Linked list をトラバースして最新の有効な StateRecord
を見つけて返します。
StateRecord の linked list の先頭に追加することができます。
MutableState の値の読み込みと書き込み
MutableState
の実体は SnapshotMutableStateImpl
というクラスです。
Getter と setter は Computed property になっています。
つまり単にフィールドの値が参照されているわけではなく、ここに秘密が隠されています。
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 }
}
}
}
MutableState.value
の Getter
override var value: T
get() = next.readable(this).value
next は StateRecord の Linked list の先頭を表しています。
readable
validかつ最新の StateRecord を返しつつ、ReadObserverの呼び出しを行っています。
MutableState.value
の Setter
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
withCurrent
で通知をすることなくvalidかつ最新の StateRecord
を取得します。
policy に基づいて StateRecord の値とセットしようとしている値が異なる場合には overwritable
で新しい StateRecrod を作成し、Linked list の先頭に追加して最新の値を書き込みます。