为什么你的App总是忘记所有事情

本文译自「Why Your App Keeps Forgetting Everything」,原文链接medium.com/mobile-app-...,由Android Dev Nexus发布于2025年6月13日。

不知你有没有发现了一个让许多 Android 开发者困惑的关键问题:ViewModel 和 savedInstanceState 解决的是不同的问题,并且拥有不同的生命周期。

让我来解释一下你的测试中究竟发生了什么,以及为什么这两种机制都存在。

Android 中的两种"死亡"类型

Android 应用可以通过两种截然不同的方式"死亡":

1. 配置变更(屏幕旋转等)

  • Activity/Fragment:死亡并重建
  • ViewModel:幸存!🎉
  • savedInstanceState:也幸存,但此时你并不需要它

2. 进程死亡(应用最小化、内存不足等)

  • Activity/Fragment:死亡
  • ViewModel:也死亡!💀
  • savedInstanceState:幸存并成为你的生命线

测试结果不撒谎

Kotlin 复制代码
// 进程死亡时会发生什么:

// 最小化应用程序之前:
class MyViewModel : ViewModel() {
    var userData = "Important data"
    var counter = 42
}
// 重新打开应用程序后:
// - 新的 ViewModel 实例已创建(旧数据已消失)
// - 但是 onCreate() 接收了已保存数据的 savedInstanceState 包
// - 你需要手动从 savedInstanceState 恢复 ViewModel

你猜测的完全正确:ViewModel 需要额外的步骤来存储/恢复数据,即使进程终止。

完整的解决方案:两者结合

以下是现代 Android 开发处理这个双层系统的方法:

Kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: MyViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        viewModel = ViewModelProvider(this)[MyViewModel::class.java]
        
        // 检查我们是否正在从进程死亡中恢复
        if (savedInstanceState != null && viewModel.isEmpty()) {
            // 从 savedInstanceState 恢复 ViewModel
            val userData = savedInstanceState.getString("user_data", "")
            val counter = savedInstanceState.getInt("counter", 0)
            viewModel.restoreFromSavedState(userData, counter)
        }
    }
    
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        // 保存关键的 ViewModel 数据以避免进程死亡
        outState.putString("user_data", viewModel.userData)
        outState.putInt("counter", viewModel.counter)
    }
}

class MyViewModel : ViewModel() {
    var userData: String = ""
    var counter: Int = 0
    
    fun isEmpty(): Boolean = userData.isEmpty() && counter == 0
    
    fun restoreFromSavedState(userData: String, counter: Int) {
        this.userData = userData
        this.counter = counter
    }
}

当每个机制生效时

让我来向你展示一下不同场景下的具体情况:

场景 1:屏幕旋转

Bash 复制代码
Before rotation: 
- ViewModel: Alive with data ✅ 
- savedInstanceState: Gets saved but not really needed 

After rotation: 
- ViewModel: Same instance, data intact ✅ 
- savedInstanceState: Available but redundant 
- Result: ViewModel data is immediately available

场景2:应用程序最小化→重新打开(进程死亡)

Bash 复制代码
Before minimizing: 
- ViewModel: Alive with data ✅ 
- onSaveInstanceState(): Saves critical data to Bundle
After reopening:
- ViewModel: NEW instance, no data ❌
- savedInstanceState: Contains saved data ✅
- Result: Must restore ViewModel from savedInstanceState

场景 3:应用程序最小化 → 重新打开(进程存活)

Bash 复制代码
Before minimizing: 
- ViewModel: Alive with data ✅
After reopening:
- ViewModel: Same instance, data intact ✅
- savedInstanceState: null (no recreation happened)
- Result: ViewModel data is immediately available

那么,为什么存在这个双层系统?

ViewModel 处理常见情况:

  • 复杂的 UI 状态,你不希望在频繁旋转屏幕时丢失。
  • 任何需要耗费大量资源重新创建的内容。

savedInstanceState 处理特殊情况:

  • 进程死亡难以预测。但当它发生时,用户希望其状态能够持久保存。
  • 小而关键的数据片段(例如用户输入、滚动位置)。
  • 简单的 UI 状态,对用户体验至关重要。

Bundle 大小的现实检验

以下是一些可以帮你免去调试麻烦的事情:Bundle 并非无限大的存储空间。你大约有 1MB 的空间可用,超过这个限制会导致 崩溃,让你质疑自己的人生选择。

保持 onSavedInstanceState 数据精简。保存用户写到一半的电子邮件草稿,而不是整个联系人列表。

现代方法:SavedStateHandle

Google 意识到这种手动操作非常繁琐,因此他们创建了 SavedStateHandle :

Kotlin 复制代码
class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    
    // 这将自动保留配置更改和进程终止!
    var userData: String
        get() = savedStateHandle.get<String>("user_data") ?: ""
        set(value) = savedStateHandle.set("user_data", value)
    
    var counter: Int
        get() = savedStateHandle.get<Int>("counter") ?: 0
        set(value) = savedStateHandle.set("counter", value)
    
    // 无需手动恢复!
}

对于以下情况,为什么仍然需要手动 onSaveInstanceState?

1. 不属于 ViewModel 的 UI 特定状态

Kotlin 复制代码
override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    // 这些不属于你的业务逻辑层
    outState.putInt("scroll_position", recyclerView.computeVerticalScrollOffset())
    outState.putBoolean("is_toolbar_expanded", collapsingToolbar.isExpanded)
    outState.putParcelable("dialog_state", currentDialog?.onSaveInstanceState())
}

2. Fragment 参数和 Activity Extras

Kotlin 复制代码
// 这些需要在进程终止后继续存在,但不是 ViewModel 状态
class DetailFragment : Fragment() {
    companion object {
        fun newInstance(itemId: String) = DetailFragment().apply {
            arguments = Bundle().apply {  // 这在内部使用 savedInstanceState
                putString("item_id", itemId)
            }
        }
    }
}

3. 视图状态过于特定于 UI

Kotlin 复制代码
// 诸如 EditText 光标位置、焦点状态等。
override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putInt("edit_text_selection_start", editText.selectionStart)
    outState.putInt("edit_text_selection_end", editText.selectionEnd)
}

把手弄脏(动手试一试)

你可以强制终止进程进行测试:

bash 复制代码
# Kill your app process 
adb shell am kill com.yourpackage.name 
# Or use "Don't keep activities" in Developer Options

这将帮助你验证状态恢复是否正确进行。

关键洞察

ViewModel 非常适合配置更改,但需要帮助应对进程死亡。

现代方法是在 ViewModel 中使用 SavedStateHandle,它可以自动处理这两种情况。如果你尚未使用它,则需要手动执行 savedInstanceState → ViewModel 的恢复过程。

这个双层系统看似复杂,但实际上非常优雅:常见情况(配置更改)的快速恢复,以及罕见情况(进程死亡)的可靠恢复。

祝你编码愉快,愿你的状态始终持久!🚀

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
阿巴斯甜12 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker13 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952714 小时前
Andorid Google 登录接入文档
android
黄林晴15 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android