前言
你是否遇到过因为屏幕旋转导致输入的内容丢失的情况,是否好奇为什么有些应用放在后台好久,再切回去应用却没有重新启动?其实这都和 Activity 生命周期 有关。
了解 Activity 的生命周期后,我们就可以写出更加流畅、用户体验更好的程序,并合理管理应用资源。
返回栈与任务(Task)
Android 中的 Activity 是层叠的,每启动一个 Activity,都会覆盖在原有的 Activity 之上,按下返回键,又会销毁处于最上面的 Activity,使得下层的 Activity 会重新展示在界面。
Android 是使用任务(Task)来管理 Activity 的,一个任务就是存放了一组 Activity 实例的栈,这个栈也被称为返回栈(Back Stack)。
栈是一种后进先出的数据结构。每当我们启动了一个新的Activity 时,它就会被压入返回栈的栈顶(入栈);而每当我们按下返回键时,处于栈顶的Activity就会被弹出(出栈),使得下方的 Activity 回到前台。系统总是向用户显示处于栈顶的Activity。
Activity 状态与系统回收策略
每个 Activity 在生命周期中最多有四种状态:
-
运行状态(Resumed)
当一个 Activity 处于返回栈的栈顶时,并且对用户来说可见、可交互,它就处于运行状态。此时的 Activity 最不容易被系统回收。
-
暂停状态(Paused)
当一个 Activity 不处于返回栈的栈顶,仍然可见(比如被一个透明/对话框式的 Activity 覆盖了),但失去焦点时,它就进入了暂停状态。此时 Activity 的
onPause()方法会被调用。当内存很低的时候,系统会考虑回收此状态的 Activity。 -
停止状态(Stopped)
当一个 Activity 不处于返回栈的栈顶,并且完全不可见时(被另一个非透明的 Activity 完全覆盖),它就进入了停止状态。此时
onStop()方法会被调用。其他地方需要内存时,系统可能会回收停止状态的 Activity。 -
销毁状态(Destroyed)
当一个 Activity 被移出返回栈(用户按下返回键或代码调用
finish()方法)或因系统回收内存而被销毁时,它就处于销毁状态。系统最会回收处于这种状态的 Activity。
系统为什么要回收 Activity?
为了保证前台应用的流畅体验,系统会根据进程的优先级来回收内存。
进程的优先级从高到低分别是:持有运行状态 Activity 的进程、持有暂停状态 Activity 的进程、持有停止状态 Activity 的进程。
当系统内存不足时,会优先杀死低优先级的进程,以释放资源给其他应用。
Activity 的七个核心回调
Activity 中有7个回调方法,分别对应了 Activity 生命周期的不同阶段。
-
onCreate():该方法会在 Activity 第一次被创建的时候调用,只会调用一次。你可以在这个方法中完成一些 Activity 的初始化操作。
-
onStart():该方法会在 Activity 由不可见变为可见的时候调用。
-
onResume()。这个方法会在 Activity 准备好和用户进行交互的时候调用。也就是 Activity 位于返回栈的栈顶,并且处于运行状态时。
-
onPause()。这个方法会在系统准备去启动或者恢复另一个 Activity 的时候调用,也就是当前 Activity 即将失去焦点,进入“暂停状态”时调用。我们通常会在这个方法中释放资源,以及保存数据。这里面的代码必须非常轻量,否则会阻塞下一个 Activity 的显示。
-
onStop()。这个方法会在 Activity 完全不可见的时候调用。它和
onPause()方法的主要区别是,如果新启动的 Activity 并没有占满全屏(如对话框),那么onPause()方法会被调用,而onStop()方法并不会被调用。可以在这里执行较重的资源释放操作 -
onDestroy()。这个方法会在 Activity 被销毁之前调用,之后 Activity 的状态就变为销毁状态了。
-
onRestart()。这个方法在 Activity 由停止状态变为运行状态之前调用,也就是 Activity 被重新启动了。
除了 onRestart() 方法外,其他方法都是成对的,所以又可以将 Activity 分为三个生存期:
-
完整生存期:在
onCreate()方法和onDestroy()方法之间所经历的就是 Activity 的完整生存期。我们一般会在onCreate()方法中,完成初始化操作,在onDestroy()方法中,释放内存。 -
可见生存期:在
onStart()方法和onStop()方法之间所经历的就是 Activity 的可见生存期。在这个时期内,Activity 对于用户来说,总是可见的(可能无法交互),我们可以通过这两个方法中管理资源:在onStart()方法中对资源进行加载,onStop()方法中对资源进行释放,减少占用的内存。 -
前台生存期:在
onResume()方法和onPause()方法之间所经历的就是 Activity 的前台生存期。此时的 Activity 是运行状态,可以与用户进行交互。
Activity 生命周期的示意图:
感知生命周期变化
有了理论知识,现在来通过代码体验一下 Activity 生命周期的变化。
新建一个 Empty Views Activity 类型的项目,命名为 ActivityLifeCycleTest。再右键 com.example.activitylifecycletest 包,点击 New->Activity->Empty Views Activity,创建两个子 Activity:NormalActivity 和 DialogActivity。
修改 NormalActivity 对应的 activity_normal.xml 布局文件,添加文字用于区分不同的界面:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is a normal activity" />
</LinearLayout>
对应地,修改 DialogActivity 对应的 activity_dialog.xml 布局文件,也是添加文字:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is a dialog activity" />
</LinearLayout>
为了演示生命周期,需要将 DialogActivity 修改为对话框式的 Activity,而 NormalActivity 则保持不变。来到 AndroidManifest 清单文件中,修改 <activity> 标签的配置:
<activity
android:name=".DialogActivity"
android:theme="@style/Theme.AppCompat.Dialog"
android:exported="false" />
<activity
android:name=".NormalActivity"
android:exported="false" />
接下来修改 activity_main.xml 布局文件,添加两个按钮,一个用于启动 NormalActivity,另一个用于启动 DialogActivity:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/startNormalActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start NormalActivity" />
<Button
android:id="@+id/startDialogActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start DialogActivity" />
</LinearLayout>
最后在 MainActivity 中,为两个按钮注册点击监听器,使它们能够启动上述两个 Activity。然后重写 7 个回调方法,在方法内部简单打印日志:
class MainActivity : AppCompatActivity() {
private val tag = "MainActivity" // 标签
private lateinit var viewBinding: ActivityMainBinding // 视图绑定变量
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
// 实例化视图绑定变量
viewBinding = ActivityMainBinding.inflate(layoutInflater)
val rootView = viewBinding.root // 获取根元素
setContentView(rootView)
viewBinding.startNormalActivity.setOnClickListener {
// 点击按钮跳转至正常的 Activity
val intent = Intent(this,NormalActivity::class.java)
startActivity(intent)
}
viewBinding.startDialogActivity.setOnClickListener {
// 点击跳转至对话框式的 Activity
val intent = Intent(this,DialogActivity::class.java)
startActivity(intent)
}
}
override fun onStart() {
super.onStart()
Log.d(tag, "onStart")
}
override fun onResume() {
super.onResume()
Log.d(tag, "onResume")
}
override fun onPause() {
super.onPause()
Log.d(tag, "onPause")
}
override fun onStop() {
super.onStop()
Log.d(tag, "onStop")
}
override fun onDestroy() {
super.onDestroy()
Log.d(tag, "onDestroy")
}
override fun onRestart() {
super.onRestart()
Log.d(tag, "onRestart")
}
}
其中按钮实例的获取使用的是视图绑定,如果不知道,可以看我的这篇文章:传送门
现在运行程序,效果如图所示:
Logcat 的日志:
然后点击第一个按钮,启动NormalActivity:
此时的日志信息:
因为 NormalActivity 会把 MainActivity 完全挡住,使得 MainActivity 处于停止状态。
按下返回键,日志信息:
由于之前的 MainActivity 已经进入了停止状态,所以会这样,并且 onCreate() 方法不会执行,因为MainActivity并没有重新被创建。
点击第二个按钮,启动 DialogActivity:
打印信息:
可以看到只有 onPause() 方法被调用了,onStop() 方法并没有执行。这是因为 DialogActivity 并没有完全盖住 MainActivity,此时MainActivity只是失去焦点,仍然可见,进入了暂停状态,并没有进入停止状态。
按下返回键返回 MainActivity,也应该只有 onResume() 方法会被调用:
再次按下返回键,退出程序,打印信息:
最终销毁掉了 MainActivity。
Activity被回收了怎么办?状态恢复策略
前面我们说过,处于停止状态的 Activity 是有可能被系统回收的。
例如我们在一个 ActivityA 的基础上启动了另一个 ActivityB,ActivityA 就进入了停止状态,此时由于系统内存不足,系统将 ActivityA 回收了,此时用户按下返回键,返回了 ActivityA,此时会发生什么?
会正常显示 ActivityA,不过 ActivityA 是重新创建过的,这样就会导致一个问题:ActivityA 中的临时数据和状态丢失了(如输入框的文字、列表的滚动位置)。
两种主要的数据丢失场景:
- 进程终止 :应用在后台时,被系统杀死以回收内存。
- 配置变更 :如屏幕旋转、语言切换,系统也会销毁并重建 Activity。
这比较影响用户的体验,我们要解决它。
传统方案:onSaveInstanceState()
Activity 中提供了 onSaveInstanceState() 回调方法,它可以保证在 Activity 被销毁前被调用。而这个方法还提供了Bundle类型的参数,Bundel提供了一系列 putXxx() 方法来让我们保存数据,方法的第一个参数是键,用于之后取值,第二个参数是值,也就是保存的内容。
在 Activity 中重写 onSaveInstanceState() 方法:
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData = "Some important data."
outState.putString("data_key", tempData)
}
怎么恢复数据呢?
其实就在 onCreate() 方法的参数中,onCreate() 方法也有一个Bundle类型的参数,可以使用Bundel提供的一系列 getXxx() 方法来让我们取出数据(存值和取值的方法要一一对应)。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
Log.d(tag, "tempData is $tempData")
}
...
}
取出值之后,做相应的恢复操作就行了,比如我们可以将文本内容重新拿到输入框中。
另外Intent还可以结合Bundle一起用于传递数据,只需在把需要传递的数据放在Bundel对象中,然后把Bundel对象放在Intent对象中,这样当Intent对象传递到目标Activity时,就能在目标Activity中拿到传递的数据了。
onSaveInstanceState() 方法的局限性:
-
保存和拿取数据时,会将数据进行序列化和反序列化,这对于大量数据或是复杂的对象来说,性能开销大。
-
它的目的主要是应对进程终止,而对于屏幕旋转这类频繁的配置变更,就显得有些笨重了。
现代化方案:ViewModel + SavedStateHandle
所以我们会使用 Jetpack ViewModel。
ViewModel 是一个专为存储和管理 UI 相关数据而设计的类。它能在配置变更(如屏幕旋转)中存活下来。当 Activity 被销毁并重新创建时,新的 Activity 实例会得到同一个 ViewModel 实例,数据就能保留下来。
该怎么选择呢?
如果只是想在屏幕旋转时,保存数据,就无脑使用 ViewModel。
而又要在进程被系统杀死后,保存数据,就需要 ViewModel + SavedStateHandle 配合使用,SavedStateHandle 本质上是对 onSaveInstanceState 机制的封装,我们可以使用它来保存那些必须在进程终止后也能恢复的关键数据。