为什么你的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 的恢复过程。

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

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

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

保护原创,请勿转载!

相关推荐
花花鱼2 小时前
android studio 设置让开发更加的方便,比如可以查看变量的类型,参数的名称等等
android·ide·android studio
刘龙超5 小时前
如何应对 Android 面试官 -> 玩转 Jetpack DataBinding
android jetpack
AirDroid_cn7 小时前
OPPO手机怎样被其他手机远程控制?两台OPPO手机如何相互远程控制?
android·windows·ios·智能手机·iphone·远程工作·远程控制
尊治7 小时前
手机电工仿真软件更新了
android
xiangzhihong89 小时前
使用Universal Links与Android App Links实现网页无缝跳转至应用
android·ios
车载应用猿10 小时前
基于Android14的CarService 启动流程分析
android
没有了遇见11 小时前
Android 渐变色实现总结
android
雨白13 小时前
Jetpack系列(四):精通WorkManager,让后台任务不再失控
android·android jetpack
mmoyula15 小时前
【RK3568 驱动开发:实现一个最基础的网络设备】
android·linux·驱动开发