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
    结果: "任盈盈"

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

相关推荐
野犬寒鸦15 分钟前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
笔画人生16 分钟前
系统级整合:`ops-transformer` 在 CANN 全栈架构中的角色与实践
深度学习·架构·transformer
霖霖总总17 分钟前
[小技巧66]当自增主键耗尽:MySQL 主键溢出问题深度解析与雪花算法替代方案
mysql·算法
rainbow688925 分钟前
深入解析C++STL:map与set底层奥秘
java·数据结构·算法
程序猿追27 分钟前
深度解码计算语言接口 (ACL):CANN 架构下的算力之门
架构
程序猿追41 分钟前
深度解码AI之魂:CANN Compiler 核心架构与技术演进
人工智能·架构
wangjialelele1 小时前
平衡二叉搜索树:AVL树和红黑树
java·c语言·开发语言·数据结构·c++·算法·深度优先
驱动探索者1 小时前
linux mailbox 学习
linux·学习·算法
ringking1231 小时前
autoware-1:安装环境cuda/cudnn/tensorRT库函数的判断
人工智能·算法·机器学习
艾莉丝努力练剑2 小时前
跨节点通信优化:使用hixl降低网络延迟的实战
架构·cann