3. remember 与 rememberSaveable:缓存作用域、跨配置保存、内存泄漏防范
3.1 原理概述
remember 和 rememberSaveable 是 Jetpack Compose 中最常用的状态缓存 API。二者都用于在组合(Composition)中保留对象,但缓存作用域和生命周期截然不同:
- remember:将值缓存在当前组合的
Composition节点中。当该节点从组合树移除(即离开组合)或配置发生重大变更(如屏幕旋转、主题切换)导致重组时,缓存会被丢弃。 - rememberSaveable:内部委托
SavedStateHandle,利用Bundle机制将状态序列化保存。它能跨越配置更新(如旋转)和进程重建(如系统杀死后台应用后恢复)保留数据。
理解二者的缓存作用域是避免状态丢失和内存泄漏的前提。
核心区分:
remember 的缓存存活于 Composition 生命周期内;rememberSaveable 的缓存存活于 Activity / Fragment 生命周期内(通过 SavedState 机制),并能自动序列化/反序列化。
3.2 缓存作用域与生命周期对比
| 特性 | remember | rememberSaveable |
|---|---|---|
| 缓存作用域 | 当前 Composition 节点 | Composition + SavedStateHandle |
| 配置变更(旋转) | 丢失(需手动处理) | 保留 |
| 进程重建 | 丢失 | 保留(需序列化) |
| 序列化要求 | 无 | 需要 Parcelable / Serializable 或自定义保存器 |
| 内存泄漏风险 | 中等(持有外部引用时) | 较低(序列化后释放引用) |
3.3 实现流程与代码示例
3.3.1 基础用法
// 使用 remember:仅缓存计算结果,不保存跨配置
@Composable
fun RememberBasicExample() {
var count by remember { mutableStateOf(0) } // 旋转后重置为 0
Button(onClick = { count++ }) {
Text("点击次数: $count")
}
}
// 使用 rememberSaveable:保留跨配置变更
@Composable
fun RememberSaveableBasicExample() {
var count by rememberSaveable { mutableStateOf(0) } // 旋转后保持
Button(onClick = { count++ }) {
Text("持久化点击: $count")
}
}
3.3.2 当对象不支持序列化时
// 使用自定义 Saver
data class UserProfile(val name: String, val age: Int)
val UserProfileSaver = Saver<UserProfile, String>(
save = { "${it.name}:${it.age}" },
restore = {
val parts = it.split(":")
UserProfile(name = parts[0], age = parts[1].toInt())
}
)
@Composable
fun ProfileCard() {
var profile by rememberSaveable(stateSaver = UserProfileSaver) {
mutableStateOf(UserProfile("小明", 25))
}
// 使用 profile
}
3.4 内存泄漏防范
remember 和 rememberSaveable 若持有外部作用域的长生命周期引用(如 Context、Activity、静态单例),会导致组合无法被 GC 回收,产生内存泄漏。常见的错误模式:
高危案例:在
remember 中直接捕获 LocalContext.current 并长期持有,即使该组合已销毁,上下文对象仍然被引用。
// ❌ 内存泄漏风险
@Composable
fun LeakingExample() {
val context = LocalContext.current
remember { MyHeavyObject(context) } // context 泄漏引用
}
// ✅ 正确做法:仅在需要时使用,避免长期持有
@Composable
fun SafeExample() {
val context = LocalContext.current
val heavyObj = remember { MyHeavyObject(context.applicationContext) } // 使用 applicationContext
// ... 或者确保 heavyObj 生命周期与组合一致
}
最佳实践:
- 避免在
remember或rememberSaveable中直接引用Activity、Fragment、View等长生命周期对象。 - 若必须使用
Context,优先使用applicationContext。 - 定期使用 Android Profiler 的 Memory Analyzer 检查不可达对象。
3.5 调试技巧
// 通过日志观察 remember 与 rememberSaveable 的重置行为
@Composable
fun DebugExample() {
val key = "debug_state"
val normal = remember { Log.d("RememberTest", "remember 重建"); 0 }
val saved = rememberSaveable { Log.d("RememberTest", "rememberSaveable 重建"); 0 }
// 旋转设备后观察日志输出
}
在 Android Studio 的 Logcat 中过滤 RememberTest,可看到 remember 在旋转后重建,而 rememberSaveable 跳过重建。
3.6 常见错误与解决方案
| 错误 | 原因 | 解决 |
|---|---|---|
| 状态在旋转后丢失 | 使用 remember 存储关键 UI 状态 |
改用 rememberSaveable |
java.lang.RuntimeException: Parcelable encountered IOException writing serializable object |
对象未实现 Parcelable 且未提供 Saver |
实现 Parcelable 或自定义 Saver |
| 内存泄漏(LeakedActivity) | remember 中持有 Context 或 Activity 引用 |
使用 applicationContext 或弱引用 |
IllegalArgumentException: Key not found |
动态生成的 key 不匹配 | 为 rememberSaveable 指定稳定 key |
3.7 性能与稳定性优化
- 避免大对象存入
rememberSaveable:序列化大量数据会导致界面卡顿。若需保留大量数据,考虑使用ViewModel+SavedStateHandle或 Room 数据库。 - 使用
derivedStateOf配合remember:当需要从多个状态派生新值时,用derivedStateOf减少非必要重组。 - 按需选择缓存策略:内部组件状态(如输入框文本)用
rememberSaveable;临时计算用remember;与 UI 无关的业务逻辑放入ViewModel。
// 优化示例:使用 derivedStateOf 避免重复计算
@Composable
fun DerivedStateExample(items: List<Int>) {
val total by remember(items) {
derivedStateOf { items.sum() }
}
Text("总和: $total")
}
关键结论:
remember 适用于计算密集型、不需要跨配置保留的临时状态;rememberSaveable 是用户操作持久化的首选,但需注意序列化开销和内存泄漏。理解二者的生命周期边界,是构建稳定 Compose 应用的基础。