LoginSignup
0
0

Jetpack Compose の Snapshot

Last updated at Posted at 2023-10-12
1 / 48

本記事には間違った内容が含まれている可能性が高いため参考にする程度でお願いします。間違った点を見つけた場合はコメントなどで教えていただけると幸いです。
また一次ソースとして本記事で参考にしているJetpack Compose internals や最新の Jetpack Compose のソースコードを参照することを推奨します。


スナップショットとは

ある瞬間状態 を保持したもの

sc-239 (1).png

イラスト: 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)  // 差し替える
}

すると


状態は変化せず何も起きなくなりります :innocent:

image.png

いくらタップしてもスイッチの状態は変わりません。


でも 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 の魔法 の秘密に迫る興味深い内容になっていると思います!

:warning: 完全に理解しきれていない部分も多々あります。ご了承ください :bow:


Snapshot がどのように動作するか

Snapshot は @Composable 外でも動きます。
実際にコードで Snapshot の挙動を観察してみましょう。

Snapshot は Compose のアーキテクチャレイヤの最も下位のレベルの Compose Runtime に含まれています。

image.png


サンプルコード

@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 を呼び出し、そのスコープ内で状態の値を出力すると結果はどうなるでしょうか?
考えてみてください :clock:


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.takeSnapshotReadonlySnapshot を返すので書き込みを行うことができません。
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 の先頭に追加して最新の値を書き込みます。


さいごに


スナップショットはとてもよくできた面白いシステムです。実際にコードでいろいろ試しながら挙動を確認してみることをおすすめします :thumbsup:

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