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 或自定义保存器
内存泄漏风险 中等(持有外部引用时) 较低(序列化后释放引用)
remember 与 rememberSaveable 生命周期对比 remember 缓存 存活于 Composition 节点 节点移出组合 缓存释放 配置变更 可能触发重组 rememberSaveable 缓存 存活于 SavedStateHandle 配置变更/进程重建 序列化后恢复 Activity 销毁时清理 关键差异 remember:组合作用域,不序列化,配置变更可丢失 rememberSaveable:可跨配置变更/进程重建,需序列化

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 应用的基础。