var count by remember { mutableStateOf(0) }详解
这一行代码是 Compose 中最经典的状态声明方式,它其实包含了三个核心概念:remember、mutableStateOf、Kotlin 委托(by)。下面逐一拆解。
一、整体作用
kotlin
var count by remember { mutableStateOf(0) }创建一个可观察的状态
count,初始值为0当
count的值变化时,Compose 会自动触发重组(Recomposition),重新执行组合函数remember保证在多次重组之间不丢失这个状态
二、mutableStateOf:可观察的状态容器
1. 基本定义
kotlin
fun <T> mutableStateOf( initialValue: T, policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy() ): MutableState<T>它创建一个MutableState<T>对象:
kotlin
interface MutableState<T> : State<T> { override var value: T }你可以把它理解为一个"带监听器的盒子":
kotlin
val count = mutableStateOf(0) // 创建状态 println(count.value) // 读取:0 count.value = 5 // 写入2. 为什么是可观察的?—— 快照系统(Snapshot)
Compose 基于快照状态系统(Snapshot State):
读取
state.value时,Compose 会把这个读取操作记录到当前的组合(Composition)中,建立依赖写入
state.value时,系统会通知所有依赖它的读取方:"值变了"于是 Compose 调度一次重组,只重新执行受影响的组合函数
类比:
mutableStateOf之于 Compose,就像LiveData之于 View,区别在于LiveData需要.observe()手动订阅,而 Compose 的依赖收集是自动且细粒度的。
3.value读写触发规则
kotlin
var count by remember { mutableStateOf(0) }读:
Text("count: $count")→ 建立依赖写:
onClick = { count++ }→ 触发重组by委托让我们直接写count而不是count.value
三、remember:跨重组缓存值
1. 问题背景
组合函数可能被频繁调用(重组)。如果每次都新建对象,状态就丢了:
kotlin
@Composable fun Counter() { // ❌ 每次重组 count 都变回 0! var count = mutableStateOf(0) }2.remember的作用
kotlin
val count = remember { mutableStateOf(0) }第一次组合:执行 lambda,创建
MutableState,存入组合后续重组:直接返回缓存的对象,跳过 lambda
组合被移除(如导航离开、LazyColumn 滚动出屏幕):缓存清除
可以理解为 Compose 内部维护了一个键值缓存:
kotlin
// 伪代码,概念示意 slotTable[index] = remember 的缓存槽3. 带 keys 的 remember
当 key 变化时,缓存作废、重新执行 lambda:
kotlin
// 用户切换时,头像重新加载 val avatar = remember(userId) { loadAvatar(userId) }kotlin
// 常用:配置变化(如深色模式)后重建数据 val data = remember(isDarkTheme) { buildThemeData(isDarkTheme) }⚠️ 注意:
rememberSaveable用 inputs 做 key 时要小心,参见下方对比表。
4. 常见用途
kotlin
remember { mutableStateOf(0) } // 状态 remember { derivedStateOf { list.filter { ... } } } // 派生状态 rememberCoroutineScope() // 协程作用域 rememberLauncherForActivityResult(...) // Activity Result rememberScrollState() // 滚动状态 LaunchedEffect(Unit) { } // 副作用(也是一种 remember 机制)四、by:Kotlin 委托属性
这行代码用了 Kotlin 的委托属性(Delegated Properties):
kotlin
var count by remember { mutableStateOf(0) } // 等价于 val countState = remember { mutableStateOf(0) } var count: Int get() = countState.value set(value) { countState.value = value }编译器会调用:
读
count→getValue(this, ::count)写
count→setValue(this, ::count, value)
MutableState通过扩展函数提供了这两个操作符:
kotlin
operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) { this.value = value }需要导入:
kotlin
import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue💡 用
var ... by(而非val ... =)的好处:代码更简洁,但把 State 传给子组合时需要by remember { ... }持有者模式(见下文状态提升)。
五、完整示例
kotlin
import androidx.compose.runtime.* @Composable fun Counter() { // 1. 声明状态:remember 缓存 + mutableStateOf 可观察 var count by remember { mutableStateOf(0) } Column { // 2. 读取 → 建立依赖 Text("点击次数: $count") // 3. 写入 → 触发重组 Button(onClick = { count++ }) { Text("点我 +1") } } }执行流程:
首次组合: remember 创建 state(0) → 显示 "0" 点击按钮: count = 1 → snapshot 通知 → 调度重组 重组: remember 返回缓存的 state → 显示 "1" 再次点击: count = 2 → 重组 → 显示 "2"六、进阶:相关状态 API 对比
| API | 跨重组保留 | 跨配置变化保留 | 用途 |
|---|---|---|---|
remember { mutableStateOf() } | ✅ | ❌(如旋转屏幕会丢) | 普通 UI 状态 |
rememberSaveable { mutableStateOf() } | ✅ | ✅(存入Bundle) | 需要 survives 配置变化/进程重建的状态 |
derivedStateOf { } | (依赖其他 state) | ❌ | 从其他状态计算派生值 |
mutableStateListOf / mutableStateMapOf | ✅ | ❌ | 可观察的集合 |
collectAsState()/collectAsStateWithLifecycle() | ✅ | ❌ | Flow → Compose 状态 |
produceState { } | ✅ | ❌ | 异步加载数据到状态 |
1.rememberSaveable
kotlin
// 旋转屏幕 / 进程被杀重建后仍保留 var count by rememberSaveable { mutableStateOf(0) }⚠️ 局限:只能存
Bundle支持的类型(基本类型、Parcelable 等)。
2.derivedStateOf:派生状态
当状态变化频率高、但 UI 只需关注变化后的结果时使用,避免过度重组:
kotlin
val query by remember { mutableStateOf("") } // 只有 isEmpty 的结果变化时才触发重组,而不是每次 query 变化 val isQueryEmpty by remember { derivedStateOf { query.isEmpty() } }3. 可观察集合
kotlin
val items = remember { mutableStateListOf<String>() } items.add("A") // ✅ 触发重组 // 普通 mutableListOf + remember 不会触发重组!七、常见坑
坑 1:忘记remember
kotlin
var count = mutableStateOf(0) // ❌ 每次重组重置为 0坑 2:在非组合作用域读写 state
kotlin
Button(onClick = { count++ }) // ✅ 事件回调中写入没问题 Text("$count") // ✅ 组合中读取没问题读取必须在组合(或 snapshot observer)中进行,否则无法建立依赖。
坑 3:状态应该放在哪里?—— 状态提升(State Hoisting)
如果一个状态被多个组合共享、或需要业务逻辑控制,应该上移到调用方:
kotlin
// 无状态(stateless)的可复用组合 @Composable fun Counter(count: Int, onCountChange: (Int) -> Unit) { Button(onClick = { onCountChange(count + 1) }) { Text("count: $count") } } // 状态持有者 @Composable fun CounterScreen() { var count by remember { mutableStateOf(0) } Counter(count = count, onCountChange = { count = it }) }好处:可预览、可测试、状态来源单一。
坑 4:remember的 key 用不稳定对象
kotlin
val user = User(name = "Tom") remember(user) { ... } // ⚠️ user 每次重组都是新对象 → 缓存永远失效应使用稳定的 key,如user.id。
坑 5:把remember当性能优化工具到处用
remember缓存的是对象创建;对纯计算用remember(key),但读 state 本身不需要 remember。滥用remember(expensive)可能反而引入错误 key 的 bug。
八、一句话总结
mutableStateOf负责"变了能通知"(可观察),remember负责"重组时不丢"(缓存),by负责"写起来简洁"(Kotlin 委托)。三者结合,就是 Compose 声明式 UI 的状态基石:你只管改数据,界面自动刷新。
Composeremember与mutableStateOf原理详解
一、整体架构概览
Compose 的响应式系统由三层构成:
text
┌─────────────────────────────────────────┐ │ SlotTable(槽表) - 存储 remember 值 │ ├─────────────────────────────────────────┤ │ Snapshot(快照系统) - 状态读写追踪 │ ├─────────────────────────────────────────┤ │ Recomposer(重组器) - 调度重组 │ └─────────────────────────────────────────┘
核心流程:读状态时记录观察者 → 写状态时通知观察者 → Recomposer 触发重组。
二、remember原理
1. SlotTable(槽表)
remember依赖 Compose 编译期的位置记忆化(Positional Memoization),背后是SlotTable数据结构。
SlotTable本质是两个并行数组:
groups: IntArray— 记录 group 的 key、size、状态标志slots: Array<Any?>— 实际存储的数据
每个@Composable函数在编译期会被包成startRestartGroup()/endRestartGroup():
kotlin
@Composable fun MyComposable() { val composer = currentComposer composer.startRestartGroup(0x1234) // 分配 group // ... body composer.endRestartGroup() }2.remember的实现
kotlin
@Composable inline fun <T> remember( crossinline calculation: @DisallowComposableCalls () -> T ): T = currentComposer.cache(false, calculation)
ComposerImpl.cache的精简逻辑:
kotlin
override fun <T> cache(invalidate: Boolean, calculation: () -> T): T { val slot = nextSlot() // 移动 slot 指针,取当前位置 return if (invalidate || slot === EMPTY) { val value = calculation() // 首次:执行 lambda updateSlot(slot, value) // 写回槽表 value } else { @Suppress("UNCHECKED_CAST") slot as T // 后续:直接复用 } }3. 关键点:位置决定身份
每次调用remember,都会按执行顺序消费一个 slot:
kotlin
@Composable fun Counter() { val a = remember { 1 } // slot[0] val b = remember { 2 } // slot[1] }重组时,只要调用顺序不变,slot 指针就能匹配到相同位置,读到之前存的值。
⚠️ 这就是为什么不能在
if/for中调用remember和 Composable:位置会漂移,导致 slot 错位,读到的可能是别的值。
4. 带 key 的 remember
kotlin
@Composable inline fun <T> remember(key1: Any?, calculation: () -> T): T { val composer = currentComposer val invalidate = composer.changed(key1) // key 变化返回 true return composer.cache(invalidate, calculation) }composer.changed(key)会把 key 存进槽表并与上次比较:不等则返回 true → 触发重新计算。
三、mutableStateOf原理
1. 创建
kotlin
fun <T> mutableStateOf( value: T, policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy() ): MutableState<T> = createSnapshotMutableState(value, policy)
返回的是ParcelableSnapshotMutableState/SnapshotMutableStateImpl(Android 上是 Parcelable 版本)。
2. SnapshotMutableStateImpl 核心结构
kotlin
private class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> ) : StateObject, MutableState<T> { private var next: StateStateRecord<T> = StateStateRecord(value) override var value: T get() = next.readable(this).value set(newValue) = next.withCurrent { current -> if (!policy.equivalent(current.value, newValue)) { next.overwritable(this, current) { this.value = newValue } } } }关键:真正的值存在StateRecord链表中,next指向当前快照下的有效记录。
3. StateRecord:快照隔离的基石
每个 StateRecord 有:
kotlin
abstract class StateRecord { var snapshotId: Int = INVALID_SNAPSHOT var next: StateRecord? = null // 链表 }当不同 Snapshot 修改同一个 State 时,会分叉成多条 Record:
text
Global Snapshot ──▶ Record1(value=1) Snapshot A ──▶ Record2(value=2) // A 的修改 Snapshot B ──▶ Record3(value=3) // B 的修改
readable(this)会遍历链表,找到当前快照可见的最新 Record。这实现了快照隔离:不同快照读到不同值,互不干扰。
4. 默认使用 GlobalSnapshot
日常代码里state.value读写走的是Snapshot.current(通常是GlobalSnapshot),所以是立即可见的。
四、读取追踪(readObserver)
1. 重组作用域:RecomposeScopeImpl
每个 Composable 在编译后都有一个RecomposeScopeImpl,它负责:
记录「我读了哪些 State」
当这些 State 变化时,把自己标记为「无效 → 需要重组」
2. 读取时如何记录
当 Composable 执行state.value时:
text
state.value │ ▼ Snapshot.current.readObserver?.invoke(state) │ ▼ Recomposer 在组合期间设置的 observer │ ▼ currentRecomposeScope.recordRead(state) │ ▼ state.recordRead(reader) → 把 scope 注册到 State 的观察者列表
精简版:
kotlin
// RecomposeScopeImpl override fun recordRead(state: StateObject) { // 只有当读取发生在当前正在组合的 scope 内才记录 observations?.add(state) // 存到 scope 的观察集 state.recordRead(this) // 反向:State 也持有 observer 引用 }Composer 在组合前会设置观察器:
kotlin
// 组合开始时 Snapshot.observe(readObserver = { state -> currentScope.recordRead(state) })五、写入与失效
1. 写入路径
kotlin
state.value = newValue │ ▼ withCurrent { current -> if (!policy.equivalent(current.value, newValue)) { overwritable(this, current) { this.value = newValue } } } │ ▼ 创建/复用 StateRecord,写入新值 │ ▼ notifyObservers / 标记失效 │ ▼ 所有注册的 RecomposeScopeImpl 被 invalidate() │ ▼ Recomposer 通过 MonotonicFrameClock 调度重组2. 结构相等策略
默认structuralEqualityPolicy()会用==比较,值相同就不触发重组:
kotlin
val s = mutableStateOf(0) s.value = 0 // 相等,不触发失效 s.value = 1 // 不等,触发
其他策略:
referentialEqualityPolicy()— 用===neverEqualPolicy()— 永远不等,每次都触发
3. Apply Observer(全局快照提交时通知)
kotlin
// Recomposer 初始化时 Snapshot.registerApplyObserver { changed, snapshot -> // 找到受影响的 RecomposeScope,标记 invalid ... recomposer.scheduleRecompose() }六、完整的一次状态更新流程
kotlin
@Composable fun Counter() { val count = remember { mutableStateOf(0) } Button(onClick = { count.value++ }) { Text("$count") // ① 读取 count.value } }首次组合:
remember在 SlotTable 中创建SnapshotMutableStateImpl并存储。读取
count.value:Text所在 scope 被注册为该 State 的观察者。点击按钮:
count.value++→ 写入新 Record → 通知观察者。Recomposer 调度:通过 frame clock 安排下一帧重组。
重组:只重组
Text所在的最小 scope。再次读取:
remember从槽表拿到同一个 State 实例,读到新值,UI 刷新。
七、几个常见问题的答案
Q1:为什么 recomposition 后 remember 的值不会丢?
因为它存在SlotTable里,而不是存在局部变量栈上。重组只是重新执行 Composable 函数体,remember会从槽表里取回上次的值。
Q2:为什么remember在if里会出错?
kotlin
if (cond) { val a = remember { ... } // 位置不稳定 } val b = remember { ... } // 位置会被 a 挤占SlotTable 是按调用顺序对齐的,条件分支会让 slot 指针错位,导致b读到a的 slot。
解决:用remember(key) {}或key(cond) { ... }显式提供位置标识。
Q3:Snapshot 系统为什么这么复杂(Record 链表)?
为了支持:
事务性修改:
Snapshot.takeMutableSnapshot()可以隔离修改,apply()才提交。多线程并发:不同线程用不同快照,互不阻塞。
Compose 的原子性:一次重组期间看到的状态是一致的快照。
Q4:by委托是怎么回事?
kotlin
var count by remember { mutableStateOf(0) }编译期转成:
kotlin
val state = remember { mutableStateOf(0) } var count: Int get() = state.value set(v) { state.value = v }读写仍然走getValue/setValue,追踪机制完全一样。
八、总结对比
| 组件 | 职责 | 关键数据结构 |
|---|---|---|
remember | 跨重组保留值 | SlotTable(数组 + 位置指针) |
mutableStateOf | 可观察状态 | StateRecord 链表 + Snapshot |
RecomposeScope | 最小重组单元 | 观察者集合 |
Recomposer | 调度重组 | 失效队列 + FrameClock |
Snapshot | 隔离与原子性 | SnapshotId + Record 链表 |
一句话总结:
remember靠「槽表 + 位置」保留对象;mutableStateOf靠「快照 + Record 链表」保存值;读时把当前RecomposeScope注册为观察者,写时通知它失效,Recomposer再精确地只重组这一个 scope。
这就是 Compose 能做到细粒度、可预测、高性能重组的根本原因。
深入篇:重组跳过机制、Snapshot 并发隔离、produceState
一、重组的细粒度跳过机制(Recomposition Skipping)
1. 什么是"跳过"
重组不等于"整个界面重画"。Compose 会只重新执行状态真正影响到的组合函数,其余直接跳过。
kotlin
@Composable fun Screen() { var count by remember { mutableStateOf(0) } var name by remember { mutableStateOf("Tom") } Column { Counter(count) // count 变化 → 重组 Greeting(name) // count 变化时,Greeting 会被跳过! } }当count变化时:
plain
Screen 重组 ├─ Counter(count) → 重新执行(读到了新 count) ├─ Greeting(name) → 跳过(参数 name 没变化,且函数内没有读变化的 state) └─ Column 内部布局 → 按需重测2. 跳过生效的条件
一个组合函数能被跳过,需要满足:
| 条件 | 说明 |
|---|---|
| 函数没有读取发生变化的 state | 依赖追踪决定"谁需要重组" |
| 所有参数都满足equals 相等 | 编译器用equals对比参数 |
| 函数没有副作用依赖执行(稳定的) | 非稳定类型会破坏跳过 |
3. 关键概念:稳定性(Stability)
跳过判断的核心是参数的类型稳定性:
稳定类型(Stable):
equals结果不会随时间变,且 public 属性变化会通知 Compose。如:Int、String、接口类型、被@Stable/@Immutable注解的类不稳定类型(Unstable):如普通
var属性的自定义 class
kotlin
// ❌ 不稳定:每次 Screen 重组传入新实例, // equals 结果可能不同 → UserCard 无法被跳过 class User(var name: String, var age: Int) @Composable fun Screen() { var count by remember { mutableStateOf(0) } UserCard(User("Tom", 20)) // 每次 Screen 重组都新建 User }修复方式:
kotlin
// ✅ 方式1:@Stable 注解,承诺"变化会通过 state 通知" @Stable class User(var name: String, var age: Int) // ✅ 方式2:@Immutable,承诺创建后不可变 @Immutable data class User(val name: String, val age: Int) // ✅ 方式3:用 remember 缓存实例 val user = remember { User("Tom", 20) } UserCard(user)注意:对于
data class(val 属性),Compose 编译器能自动推断稳定性;带var的普通类必须手动注解。
4.CompositionLocal的跳过规则
CompositionLocal也是隐式参数,读取了某个 Local 的组合会在其变化时重组:
kotlin
val isDark = LocalDarkTheme.current // LocalDarkTheme 变化 → 重组这与显式传参二选一:参数 vs CompositionLocal是 Compose 架构的经典权衡——显式参数利于测试和跳过,Local 利于跨层传递。
5. 反向确认:什么情况下会被跳过失效
kotlin
@Composable fun Bad(count: Int) { val context = LocalContext.current // 读取了 context(隐式参数),context 变化时即使 count 不变也会重组 }还有inline fun组合(inline 后无法跳过)、读取不稳定类型等。
二、Snapshot 的并发隔离
1. Snapshot 不只是"通知",它是一个 MVCC 事务系统
Compose 的状态系统底层是Snapshot(快照),设计目标有二:
细粒度依赖追踪:谁读了 state,值变了通知谁
并发隔离:多线程读写 state 时不互相污染
它类似数据库的MVCC(多版本并发控制):
线程 A 修改 count: 1 → 2 → 在 A 的快照中 count=2 线程 B 此刻读 count → 在 B 的快照中 count 仍是 1(隔离) A 提交快照 → 全局 state 变为 2,B 下次读取看到 22. 核心 API
快照内的修改不会立即全局生效
kotlin
val state = mutableStateOf(0) val snapshot = Snapshot.takeSnapshot() try { snapshot.enter { state.value = 100 // 在这个快照里改成 100 println(state.value) // 100 } println(state.value) // 仍然是 0!还没提交 snapshot.apply() // 提交,全局生效 println(state.value) // 100 } finally { snapshot.dispose() }如果在enter块中崩溃,直接dispose()即可丢弃修改——相当于事务回滚。
并行修改冲突
两个快照同时改同一个 state,后提交的会失败(乐观并发控制):
kotlin
val s1 = Snapshot.takeSnapshot() val s2 = Snapshot.takeSnapshot() s1.enter { state.value = 10 } s2.enter { state.value = 20 } s1.apply() // ✅ 成功,全局 state = 10 s2.apply() // ❌ 失败!抛出异常(或返回 false) // 因为 s2 基于的旧值已被 s1 改掉3. Snapshot 与重组的关系
重组其实运行在一个快照观察者环境里:
plain
组合函数执行时:读取 state → 记录依赖(Snapshot 观察者) state 变化: 快照系统比对 → 通知观察者 → 调度重组这就是为什么只能在组合/观察者环境中读取 state才能建立依赖;在LaunchedEffect等副作用里读 state 并不会触发重组,需要用snapshotFlow { }桥接:
kotlin
LaunchedEffect(Unit) { // ✅ 把 state 转成 Flow:state 变化 → Flow 发射 → 副作用响应 snapshotFlow { query } .debounce(300) .collect { result -> search(result) } }4. 线程安全性
mutableStateOf的读写是线程安全的(内部有同步机制),但注意:
kotlin
// ✅ 可以:后台线程改 state,Compose 会自动调度重组 scope.launch(Dispatchers.IO) { count = 5 }不需要切回主线程,Snapshot 系统保证一致性,且 Compose 会智能地把重组调度到 UI 线程。
三、produceState:异步加载数据到状态
1. 解决什么问题
remember { mutableStateOf() }适合同步初始化的数据;但数据来自网络、数据库、回调时,需要异步加载——produceState就是为这个场景设计的:
kotlin
@Composable fun UserScreen(userId: String) { // 返回一个 State<User?>,加载中/完成/失败都能表达 val user by produceState<User?>(initialValue = null, userId) { value = repository.fetchUser(userId) // value 就是 MutableState.value } when { user == null -> Loading() else -> UserCard(user!!) } }2. 等价展开(理解原理)
produceState本质是一个语法糖:
kotlin
// 伪代码展开 @Composable fun <T> produceState(initialValue: T, key: Any?, producer: suspend ProduceStateScope<T>.() -> Unit): State<T> { val result = remember(key) { mutableStateOf(initialValue) } LaunchedEffect(key) { ProduceStateScopeImpl(result).producer() } return result }所以记住它的三个特性:
| 特性 | 说明 |
|---|---|
remember(key) | key 变化(如userId变了)→ 重置为初始值并重新加载 |
LaunchedEffect(key) | 加载逻辑在协程里执行,可挂起 |
作用域value | 可直接赋值,也可try/catch处理异常 |
3. 完整实战模式
kotlin
sealed interface UiState<out T> { data object Loading : UiState<Nothing> data class Success<T>(val data: T) : UiState<T> data class Error(val message: String) : UiState<Nothing> } @Composable fun <T> rememberData( key: Any?, block: suspend () -> T ): State<UiState<T>> = produceState<UiState<T>>(initialValue = UiState.Loading, key) { value = try { UiState.Success(block()) } catch (e: Exception) { UiState.Error(e.message ?: "未知错误") } } // 使用 @Composable fun RepoScreen(repoId: String) { val state by rememberData(repoId) { repository.getRepo(repoId) } when (val s = state) { UiState.Loading -> LoadingView() is UiState.Error -> ErrorView(s.message) is UiState.Success -> RepoCard(s.data) } }4.produceStatevs 其他方案
| 方案 | 适用场景 |
|---|---|
produceState | 组合内的一次性异步加载(无 ViewModel) |
ViewModel+collectAsStateWithLifecycle() | 有业务层、需跨组合存活(生产环境首选) |
LaunchedEffect+mutableStateOf | 手动版 produceState,需要更精细控制时 |
collectAsState(initial) | 把已有 Flow 直接转 State |
kotlin
// 生产环境典型写法(ViewModel 持有状态流) @Composable fun RepoScreen(viewModel: RepoViewModel = viewModel()) { val state by viewModel.uiState.collectAsStateWithLifecycle() // ... }produceState适合简单页面、预览、或没有架构层的场景;一旦状态需要跨页面共享、需要缓存策略、需要处理复杂生命周期,就交给 ViewModel。
四、三者串起来:一条完整的数据流
① produceState / snapshotFlow 负责"数据怎么来" ↓ ② mutableStateOf + remember 负责"数据怎么存、谁能看见" ↓ (Snapshot 追踪依赖,变化自动通知) ③ 重组调度 负责"谁需要刷新" ↓ (稳定性 + 参数对比) ④ 细粒度跳过 负责"只刷新该刷新的部分"一句话总结:Snapshot 是地基(并发安全 + 依赖追踪),跳过机制是性能优化(稳定性 + equals),produceState是异步数据入口(remember + LaunchedEffect 的组合糖)。