Compose原理一之快照系统

Compose的快照系统基于 MVCC (多版本并发控制) 思想,类似于数据库事务。

ini 复制代码
StateObject (mutableStateOf(0))
       │
       ▼
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│ Record id=7  │───►│ Record id=5  │───►│ Record id=1  │
│ value=100    │    │ value=50     │    │ value=0      │
└──────────────┘    └──────────────┘    └──────────────┘
     (最新)                                  (初始)

目标是保证:

  • 隔离性: 不同快照的修改相互隔离
  • 原子性: 快照应用是原子操作
  • 可观察性: 支持状态读写追踪

示例代码

kotlin 复制代码
class Person {
    var name: MutableState<String> = mutableStateOf("")
}

val person = Person()
person.name.value = "令狐冲"
val snapshot = Snapshot.takeSnapshot()
person.name.value = "任盈盈"

println("第一次打印:${person.name.value}")  // 输出: 任盈盈
snapshot.enter {
    println("第二次打印:${person.name.value}")  // 输出: 令狐冲
}
println("第三次打印:${person.name.value}")  // 输出: 任盈盈

第二次打印输出令狐冲,说明快照保存之前的状态,快照是如何保存之前的状态?第三次打印任盈盈,说明enter方法执行完成后,又恢复成最新状态了,这又是为何?下面将一步一步的解析示例中的每一行代码。

第一步:创建 MutableState (mutableStateOf(""))

当执行 mutableStateOf("") 时:

源码位置:SnapshotState.kt

kotlin 复制代码
public fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(),
): MutableState<T> = createSnapshotMutableState(value, policy)

源码位置:SnapshotState.kt

kotlin 复制代码
internal open class SnapshotMutableStateImpl<T>(
    value: T,
    override val policy: SnapshotMutationPolicy<T>,
) : StateObjectImpl(), SnapshotMutableState<T> {
    
    private var next: StateStateRecord<T> =
        currentSnapshot().let { snapshot ->
            StateStateRecord(snapshot.snapshotId, value).also {
                if (snapshot !is GlobalSnapshot) {
                    it.next = StateStateRecord(Snapshot.PreexistingSnapshotId.toSnapshotId(), value)
                }
            }
        }
    // ...
}

发生了什么?

  1. 获取当前快照 : currentSnapshot() 返回 GlobalSnapshot(假设 id=1)
  2. 创建 StateRecord : 创建 StateStateRecord(snapshotId=1, value="")
  3. 内存结构:
ini 复制代码
person.name (SnapshotMutableStateImpl)
    │
    └── next ──► StateStateRecord
                   ├── snapshotId = 1
                   ├── value = ""
                   └── next = null

第二步:第一次赋值 (person.name.value = "令狐冲")

源码位置:SnapshotState.kt

kotlin 复制代码
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 }
            }
        }

value = "令狐冲" 时:

  1. 调用 withCurrent: 找到当前可读的记录
  2. 检查等价性 : policy.equivalent("", "令狐冲") 返回 false
  3. 调用 overwritable

源码位置:Snapshot.kt

kotlin 复制代码
internal inline fun <T : StateRecord, R> T.overwritable(
    state: StateObject,
    candidate: T,
    block: T.() -> R,
): R {
    val snapshot: Snapshot
    return sync {
            snapshot = Snapshot.current  // GlobalSnapshot (id=1)
            this.overwritableRecord(state, snapshot, candidate).block()
        }
        .also { notifyWrite(snapshot, state) }
}

源码位置:Snapshot.kt

kotlin 复制代码
internal fun <T : StateRecord> T.overwritableRecord(
    state: StateObject,
    snapshot: Snapshot,
    candidate: T,
): T {
    val id = snapshot.snapshotId  // id=1

    // 如果 candidate 已经是当前快照创建的,直接返回
    if (candidate.snapshotId == id) return candidate

    // 否则创建新记录
    val newData = sync { newOverwritableRecordLocked(state) }
    newData.snapshotId = id
    // ...
    return newData
}

关键点

因为 candidate.snapshotId == 1 等于 snapshot.snapshotId == 1直接修改现有记录

执行后的内存结构

ini 复制代码
person.name (SnapshotMutableStateImpl)
    │
    └── next ──► StateStateRecord
                   ├── snapshotId = 1
                   ├── value = "令狐冲"  ← 被覆盖
                   └── next = null

第三步:创建快照 (Snapshot.takeSnapshot())

源码位置:Snapshot.kt

kotlin 复制代码
public fun takeSnapshot(readObserver: ((Any) -> Unit)? = null): Snapshot =
    currentSnapshot().takeNestedSnapshot(readObserver)

调用 GlobalSnapshot.takeNestedSnapshot()

源码位置:Snapshot.kt

kotlin 复制代码
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot =
    creatingSnapshot(...) { actualReadObserver, _ ->
        takeNewSnapshot { invalid ->
            ReadonlySnapshot(
                snapshotId = sync { nextSnapshotId.also { nextSnapshotId += 1 } },  // id=2
                invalid = invalid,
                readObserver = actualReadObserver,
            )
        }
    }

源码位置:Snapshot.kt

kotlin 复制代码
private fun <T : Snapshot> takeNewSnapshot(block: (invalid: SnapshotIdSet) -> T): T =
    advanceGlobalSnapshot { invalid ->
        val result = block(invalid)
        sync { openSnapshots = openSnapshots.set(result.snapshotId) }
        result
    }

源码位置:Snapshot.kt

kotlin 复制代码
private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
    val globalSnapshot = globalSnapshot
    val result = sync {
        // 重置全局快照,分配新 ID
        resetGlobalSnapshotLocked(globalSnapshot, block)
    }
    // ...
    return result
}

源码位置:Snapshot.kt

kotlin 复制代码
private fun <T> resetGlobalSnapshotLocked(
    globalSnapshot: GlobalSnapshot,
    block: (invalid: SnapshotIdSet) -> T,
): T {
    val snapshotId = globalSnapshot.snapshotId  // 当前是 1
    val result = block(openSnapshots.clear(snapshotId))  // invalid = {空}
    
    val nextGlobalSnapshotId = nextSnapshotId  // 获取下一个 ID (2)
    nextSnapshotId += 1
    
    openSnapshots = openSnapshots.clear(snapshotId)
    globalSnapshot.snapshotId = nextGlobalSnapshotId  // GlobalSnapshot 变成 id=3
    globalSnapshot.invalid = openSnapshots            // {2} - 因为快照 2 还未关闭
    openSnapshots = openSnapshots.set(nextGlobalSnapshotId)  // {2, 3}
    
    return result
}

执行顺序详解

  1. 进入时: GlobalSnapshot.id=1, nextSnapshotId=2, openSnapshots={1}
  2. 创建 ReadonlySnapshot: id=2, invalid={}(空集合!)
  3. 更新 GlobalSnapshot: id=3
  4. 退出时: GlobalSnapshot.id=3, nextSnapshotId=4, openSnapshots={2,3}

关键点:snapshot 的 invalid 集合

ini 复制代码
ReadonlySnapshot:
  snapshotId = 2
  invalid = {}  ← 空集合!这意味着它能看到 id <= 2 的所有记录

执行后的状态

ini 复制代码
GlobalSnapshot: id=3, invalid={2}
ReadonlySnapshot (我们拿到的): id=2, invalid={}

person.name 的记录链:
    StateStateRecord(snapshotId=1, value="令狐冲") → null

第四步:第二次赋值 (person.name.value = "任盈盈")

再次执行 overwritable:

源码位置:Snapshot.kt

kotlin 复制代码
internal fun <T : StateRecord> T.overwritableRecord(
    state: StateObject,
    snapshot: Snapshot,
    candidate: T,
): T {
    val id = snapshot.snapshotId  // 现在是 3(GlobalSnapshot)

    // candidate.snapshotId = 1, id = 3
    // 1 != 3,需要创建新记录!
    if (candidate.snapshotId == id) return candidate

    val newData = sync { newOverwritableRecordLocked(state) }
    newData.snapshotId = id  // 设为 3
    // ...
    return newData
}

源码位置:Snapshot.kt

kotlin 复制代码
internal fun <T : StateRecord> T.newOverwritableRecordLocked(state: StateObject): T {
    return (usedLocked(state) as T?)?.apply { snapshotId = SnapshotIdMax }
        ?: create(SnapshotIdMax).apply {
            this.next = state.firstStateRecord  // 指向旧记录
            state.prependStateRecord(this as T)  // 插入链表头部
        } as T
}

执行后的内存结构

ini 复制代码
person.name (SnapshotMutableStateImpl)
    │
    └── next ──► StateStateRecord (新)
                   ├── snapshotId = 3
                   ├── value = "任盈盈"
                   └── next ──► StateStateRecord (旧)
                                  ├── snapshotId = 1
                                  ├── value = "令狐冲"
                                  └── next = null

这就是关键:旧记录被保留了!


第五步:第一次打印 (在全局快照中读取)

kotlin 复制代码
println("第一次打印:${person.name.value}")

源码位置:SnapshotState

kotlin 复制代码
override var value: T
    get() = next.readable(this).value

源码位置:Snapshot.kt

kotlin 复制代码
public fun <T : StateRecord> T.readable(state: StateObject): T {
    val snapshot = Snapshot.current  // GlobalSnapshot: id=3, invalid={2}
    snapshot.readObserver?.invoke(state)
    return readable(this, snapshot.snapshotId, snapshot.invalid)
        ?: // fallback...
}

源码位置:Snapshot.kt

kotlin 复制代码
private fun <T : StateRecord> readable(r: T, id: SnapshotId, invalid: SnapshotIdSet): T? {
    // id=3, invalid={2}
    var current: StateRecord? = r
    var candidate: StateRecord? = null
    while (current != null) {
        if (valid(current, id, invalid)) {
            candidate =
                if (candidate == null) current
                else if (candidate.snapshotId < current.snapshotId) current else candidate
        }
        current = current.next
    }
    return candidate as? T
}

源码位置:Snapshot.kt

kotlin 复制代码
private fun valid(
    currentSnapshot: SnapshotId,
    candidateSnapshot: SnapshotId,
    invalid: SnapshotIdSet,
): Boolean {
    return candidateSnapshot != INVALID_SNAPSHOT &&
        candidateSnapshot <= currentSnapshot &&
        !invalid.get(candidateSnapshot)
}

版本选择过程

GlobalSnapshot: id=3, invalid={2}

记录 snapshotId valid() 检查 结果
记录1 3 3 != 0 ✓, 3 <= 3 ✓, 3 不在 {2} ✓ 有效
记录2 1 1 != 0 ✓, 1 <= 3 ✓, 1 不在 {2} ✓ 有效

选择 snapshotId 最大的有效记录 → 记录1 (snapshotId=3, value="任盈盈")

输出: 任盈盈


第六步:进入快照 (snapshot.enter { ... })

源码位置:Snapshot.kt

kotlin 复制代码
public inline fun <T> enter(block: () -> T): T {
    val previous = makeCurrent()
    try {
        return block()
    } finally {
        restoreCurrent(previous)
    }
}

源码位置:Snapshot.kt

kotlin 复制代码
internal open fun makeCurrent(): Snapshot? {
    val previous = threadSnapshot.get()
    threadSnapshot.set(this)  // 将 ReadonlySnapshot 设为当前快照
    return previous
}

现在 Snapshot.current 返回我们的 ReadonlySnapshot (id=2, invalid={})


第七步:第二次打印 (在 ReadonlySnapshot 中读取)

kotlin 复制代码
println("第二次打印:${person.name.value}")

再次调用 readable():

ReadonlySnapshot: id=2, invalid={}

版本选择过程

记录 snapshotId valid() 检查 结果
记录1 3 3 != 0 ✓, 3 <= 2 ✗ 无效
记录2 1 1 != 0 ✓, 1 <= 2 ✓, 1 不在 {} ✓ 有效

选择唯一有效的记录 → 记录2 (snapshotId=1, value="令狐冲")

输出: 令狐冲

为什么记录1无效?

kotlin 复制代码
candidateSnapshot <= currentSnapshot
3 <= 2  // FALSE!

记录1的 snapshotId=3 大于 ReadonlySnapshot 的 id=2,所以它对这个快照是"未来"的,不可见。


第八步:退出快照并第三次打印

kotlin 复制代码
// snapshot.enter 返回后,自动调用 restoreCurrent(previous)
// Snapshot.current 又变回 GlobalSnapshot (id=3)
println("第三次打印:${person.name.value}")

与第五步相同的逻辑,输出: 任盈盈


核心原理总结

1. MVCC 多版本

每次在新快照 中修改状态,都会创建新记录而不是覆盖旧记录:

bash 复制代码
链表头 → Record(id=3, "任盈盈") → Record(id=1, "令狐冲") → null

2. 版本选择算法

readable() 函数通过三个条件选择版本:

kotlin 复制代码
candidateSnapshot != INVALID_SNAPSHOT &&  // 不是无效 ID
candidateSnapshot <= currentSnapshot &&   // 不超过当前快照 ID
!invalid.get(candidateSnapshot)           // 不在 invalid 集合中

3. invalid 集合的作用

  • 快照创建时,记录哪些快照 ID 是"不可见"的
  • 对于 ReadonlySnapshot,创建时的 invalid 是空的
  • 对于 GlobalSnapshot,invalid 包含所有未 apply 的 MutableSnapshot

4. ThreadLocal 实现线程隔离

kotlin 复制代码
private val threadSnapshot = SnapshotThreadLocal<Snapshot>()

internal fun currentSnapshot(): Snapshot = 
    threadSnapshot.get() ?: globalSnapshot

图解完整流程

ini 复制代码
时间线:
───────────────────────────────────────────────────────►

T1: mutableStateOf("")
    GlobalSnapshot id=1
    Record: [id=1, value=""]
    
T2: value = "令狐冲"  
    GlobalSnapshot id=1 (未变)
    Record: [id=1, value="令狐冲"]  ← 直接覆盖,因为同一快照
    
T3: takeSnapshot()
    GlobalSnapshot id=3 (推进了!)
    ReadonlySnapshot id=2, invalid={}
    Record: 不变
    
T4: value = "任盈盈"
    GlobalSnapshot id=3
    创建新记录!
    Record链: [id=3, value="任盈盈"] → [id=1, value="令狐冲"]
    
T5: 全局读取 (GlobalSnapshot id=3, invalid={2})
    选择: id=3 的记录 (3 <= 3 且 3 不在 {2})
    结果: "任盈盈"
    
T6: snapshot.enter 读取 (ReadonlySnapshot id=2, invalid={})
    检查 id=3: 3 <= 2? NO → 无效
    检查 id=1: 1 <= 2? YES → 有效
    结果: "令狐冲"
    
T7: 全局读取
    同 T5
    结果: "任盈盈"

这就是快照能看到"过去"状态的完整原理!

相关推荐
独处东汉22 分钟前
freertos开发空气检测仪之输入子系统结构体设计
数据结构·人工智能·stm32·单片机·嵌入式硬件·算法
乐迪信息25 分钟前
乐迪信息:AI防爆摄像机在船舶监控的应用
大数据·网络·人工智能·算法·无人机
放荡不羁的野指针29 分钟前
leetcode150题-滑动窗口
数据结构·算法·leetcode
小龙报1 小时前
【C语言进阶数据结构与算法】单链表综合练习:1.删除链表中等于给定值 val 的所有节点 2.反转链表 3.链表中间节点
c语言·开发语言·数据结构·c++·算法·链表·visual studio
奈斯ing1 小时前
【Oracle篇】基于OGG 21c全程图形化实现9TB数据从Oracle 11g到19c的不停机迁移(上):微服务架构详解与微服务部署,及同步问题总览(第一篇,总共三篇)
微服务·oracle·架构
Hernon1 小时前
微服务架构设计 - 架构取舍决策CAP
微服务·云原生·架构
LINgZone21 小时前
领域驱动设计(DDD)在架构中的应用
架构
潆润千川科技1 小时前
架构演进思考:中老年社交应用如何通过数据治理与业务解耦实现稳健增
架构·聊天小程序
潆润千川科技1 小时前
适老社交应用后端架构思考:在安全、性能与简单之间的平衡艺术
安全·架构
TracyCoder1232 小时前
LeetCode Hot100(13/100)——238. 除了自身以外数组的乘积
算法·leetcode