《Android编程权威指南》之UI状态的保存与恢复篇

787 阅读5分钟

这是我参与8月更文挑战的第14天,活动详情查看:8月更文挑战

本章主要学习使用ViewModel保存UI数据,修复GeoQuiz应用的UI状态丢失缺陷。

一、引入 ViewModel 依赖 

ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。

它来自lifecycle-extensions的Android Jetpack库,目前 lifecycle-extensions 中的 API 已弃用。您可以为特定 Lifecycle 工件添加所需的依赖项。参考:

developer.android.com/jetpack/and…

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0-alpha03

然后点击 Sync Now。

二、添加 ViewModel

  • 创建 QuizViewModel

    private const val TAG = "QuizViewModel"
    
    class QuizViewModel : ViewModel() {
    
      init {
          Log.d(TAG, "ViewModel instance created")
      }
    
      /**
       * On cleared
       * onCleared()函数的调用恰好在ViewModel被销毁之前。适合做一些善后清理工作,比如解绑某个数据源。
       */
      override fun onCleared() {
          super.onCleared()
          Log.d(TAG, "ViewModel instance about to destroyed")
      }
    
    }
    
  • 访问ViewModel

    书中访问ViewModel的方法已经被弃用了,正如前面所说,我的实践并非引入 lifecycle-extensions,因此实际代码有所小改动。

    在MainActivity.class 的 onCeate()方法中加入:

    override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_main)
          ...
          val quizViewModel by lazy { ViewModelProvider(this).get(QuizViewModel::class.java) }
    
          ...
    }
    

    只是换了api去拿quizViewModel的实例而已。

    在MainActivity首次访问QuizViewModel时,ViewModelProvider会创建并返回一 个QuizViewModel新实例。在设备配置改变之后,MainActivity再次访问QuizViewModel对象时,它返回的是之前创建的QuizViewModel。在MainActivity完成使命销毁时(比如用户按了回退键),ViewModel-Activity这对好朋友也就从内存里抹掉了。

2.1 ViewModel生命周期与ViewModelProvider 

上述代码意味着,一个ViewModel实例和一个activity生命周期已经关联,不管关联activity处于什么状态,该ViewModel会一直保留在内存里,直到关联activity被销毁。

QuizViewModel和MainActivity步调一致

设备旋转时,ViewModel 也留在了内存里。

MainActivity和QuizViewModel经历设备旋转

运行GeoQuiz应用日志:

初次打开

旋转设备日志:(可以看出viewmodel并未重建,而是从内存中直接取第一次创建的)

旋转后

退出应用日志:(viewmodel才销毁)

退出app

小总结:QuizViewModel 和 MainActivity 的关系是单向的。某个activity会引用其关联ViewModel,反过来则不行。一个ViewModel绝不能引用activity或view,否则会引发内存泄漏。

当某个对象强引用另一个要被销毁的对象时,内存泄漏就会发生。这样的强引用会阻止垃圾回收器从内存里清理对象。设备配置改变带来的内存泄漏是常见问题。

2.2 向ViewModel添加数据 

ViewModel 会保存关联用户界面所需数据,并整理格式化这些数据,以方便其他对象取用。这样就可以把屏幕展现逻辑从activity里删除,让其“瘦身”了。

class QuizViewModel : ViewModel() {

    var currentIndex = 0

    private val questionBank = listOf(
        Question(R.string.question_australia, true),
        Question(R.string.question_oceans, true),
        Question(R.string.question_mideast, false),
        Question(R.string.question_africa, false),
        Question(R.string.question_americas, true),
        Question(R.string.question_asia, true)
    )

    val currentQuestionAnswer: Boolean get() = questionBank[currentIndex].answer

    val currentQuestionText: Int get() = questionBank[currentIndex].textResId

    fun moveToNext() {
        currentIndex = (currentIndex + 1) % questionBank.size
    }
}

使用by lazy关键字,可以确保quizViewModel属性是val类型,而不是var类型。只在activity实例对象被创建后,才需要获取和保存QuizViewModel,也就是说,quizViewModel一次只赋一个值。

三、进程销毁时保存数据

上面讲述的是发生屏幕旋转等配置更改的情况下,activity会被销毁和重启,这个时候可以用viewmodel来自动保存数据与获取数据。但是,如果是整个Android系统内存不够用的情况下,app又不在前台,系统是可能直接清除掉整个app的进程,这个时候,viewmodel 就不管用了,因为它也不在了。

3.1 覆盖onSaveInstanceState(Bundle)函数

通过覆盖Activity.onSaveInstanceState(Bundle)的方式,就可以解决上述问题,当应用进程在意外被系统“杀死”的时候,帮用户保存一些不是很大的关键数据,从而在再次加载app的时候恢复状态。

3.2 保留实例状态与activity记录

增加一个暂存状态(stashed state)到activity生命周期:

完整的activity生命周期

注意,activity进入暂存状态并不一定需要调用onDestroy()函数。不过,onStop()和onSaveInstanceState(Bundle)是两个可靠的函数(除非设备出现重大故障)。

通常,覆盖onSaveInstanceState(Bundle)函数,在Bundle对象中,保存当前activity小的或暂存状态的数据;覆盖onStop()函数,保存永久性数据,比如用户编辑的文字等。

要测试系统内存不够杀死应用,进入开发者选项,将不保留活动开启,那么在应用启动后,点击了home键,系统就是自动去杀死app了。如图设置:

不保留活动

四、ViewModel与保存实例状态

保留实例状态和ViewModel都不是长期存储解决方案。如果应用需要长久存储数据,且完全不担心activity状态,那么请考虑使用持久化存储方案。(后续会学)

ViewModel 始终还是对内存数据进行操作,所以速度上来说会占优势,加上书中的GeoQuiz应用例子,题目都是硬编码的,不是从网络获取,而且数据也不多,不需要数据库来存储,因此对于此应用来说,使用ViewModel是方案还是很合理的。

因此,要处理设备配置更改 加上 系统发起的进程终止 两种情况,就结合使用 ViewModel 和 onSaveInstanceState() 方式来保存数据状态。

五、深入学习:Jetpack、AndroidX与架构组件

Jetpack库分为四大类:foundation、architecture、behavior和UI。

architecture类Jetpack库还有一个常见名字叫architecture component。ViewModel就是一种架构组件。

参考:developer.android.com/jetpack

六、深入学习:解决问题要彻底

意思就是通过禁止应用屏旋转,以此解决设备配置改变带来的UI状态丢失问题的方式太粗暴,也不能从根本解决问题,这也解决不了决进程销毁问题,在开发过程中,还会遇到其他的跟生命周期有关的问题,我们得查到根本,然后多学一些知识技术点,来解决开发问题!

七、最后

本篇的实践代码地址:

github.com/visiongem/A…

由于会按照自己的意思实践练习题,因此跟书中示例代码有少许不一样,仅供参考。有兴趣就自行完善喽。实践出真知呢!给自己加个油!🆙


🌈关注我吖~❤️

公众号:妮K妮K妮