深入解析Activity的生命周期

1,242 阅读10分钟

前言

你是否遇到过因为屏幕旋转导致输入的内容丢失的情况,是否好奇为什么有些应用放在后台好久,再切回去应用却没有重新启动?其实这都和 Activity 生命周期 有关。

了解 Activity 的生命周期后,我们就可以写出更加流畅、用户体验更好的程序,并合理管理应用资源。

返回栈与任务(Task)

Android 中的 Activity 是层叠的,每启动一个 Activity,都会覆盖在原有的 Activity 之上,按下返回键,又会销毁处于最上面的 Activity,使得下层的 Activity 会重新展示在界面。

Android 是使用任务(Task)来管理 Activity 的,一个任务就是存放了一组 Activity 实例的栈,这个栈也被称为返回栈(Back Stack)

栈是一种后进先出的数据结构。每当我们启动了一个新的Activity 时,它就会被压入返回栈的栈顶(入栈);而每当我们按下返回键时,处于栈顶的Activity就会被弹出(出栈),使得下方的 Activity 回到前台。系统总是向用户显示处于栈顶的Activity。

image.png

Activity 状态与系统回收策略

每个 Activity 在生命周期中最多有四种状态:

  1. 运行状态(Resumed)

    当一个 Activity 处于返回栈的栈顶时,并且对用户来说可见、可交互,它就处于运行状态。此时的 Activity 最不容易被系统回收。

  2. 暂停状态(Paused)

    当一个 Activity 不处于返回栈的栈顶,仍然可见(比如被一个透明/对话框式的 Activity 覆盖了),但失去焦点时,它就进入了暂停状态。此时 Activity 的 onPause() 方法会被调用。当内存很低的时候,系统会考虑回收此状态的 Activity。

  3. 停止状态(Stopped)

    当一个 Activity 不处于返回栈的栈顶,并且完全不可见时(被另一个非透明的 Activity 完全覆盖),它就进入了停止状态。此时 onStop() 方法会被调用。其他地方需要内存时,系统可能会回收停止状态的 Activity。

  4. 销毁状态(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 生命周期的示意图:

image.png

感知生命周期变化

有了理论知识,现在来通过代码体验一下 Activity 生命周期的变化。

新建一个 Empty Views Activity 类型的项目,命名为 ActivityLifeCycleTest。再右键 com.example.activitylifecycletest 包,点击 New->Activity->Empty Views Activity,创建两个子 Activity:NormalActivityDialogActivity

修改 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")
    }
}

其中按钮实例的获取使用的是视图绑定,如果不知道,可以看我的这篇文章:传送门

现在运行程序,效果如图所示:

image.png

Logcat 的日志:

image.png

然后点击第一个按钮,启动NormalActivity:

image.png

此时的日志信息:

image.png

因为 NormalActivity 会把 MainActivity 完全挡住,使得 MainActivity 处于停止状态。

按下返回键,日志信息:

image.png

由于之前的 MainActivity 已经进入了停止状态,所以会这样,并且 onCreate() 方法不会执行,因为MainActivity并没有重新被创建。

点击第二个按钮,启动 DialogActivity:

image.png

打印信息:

image.png

可以看到只有 onPause() 方法被调用了,onStop() 方法并没有执行。这是因为 DialogActivity 并没有完全盖住 MainActivity,此时MainActivity只是失去焦点,仍然可见,进入了暂停状态,并没有进入停止状态。

按下返回键返回 MainActivity,也应该只有 onResume() 方法会被调用:

image.png

再次按下返回键,退出程序,打印信息:

image.png

最终销毁掉了 MainActivity。

Activity被回收了怎么办?状态恢复策略

前面我们说过,处于停止状态的 Activity 是有可能被系统回收的。

例如我们在一个 ActivityA 的基础上启动了另一个 ActivityB,ActivityA 就进入了停止状态,此时由于系统内存不足,系统将 ActivityA 回收了,此时用户按下返回键,返回了 ActivityA,此时会发生什么?

会正常显示 ActivityA,不过 ActivityA 是重新创建过的,这样就会导致一个问题:ActivityA 中的临时数据和状态丢失了(如输入框的文字、列表的滚动位置)。

两种主要的数据丢失场景:

  1. 进程终止 :应用在后台时,被系统杀死以回收内存。
  2. 配置变更 :如屏幕旋转、语言切换,系统也会销毁并重建 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() 方法的局限性

  1. 保存和拿取数据时,会将数据进行序列化和反序列化,这对于大量数据或是复杂的对象来说,性能开销大。

  2. 它的目的主要是应对进程终止,而对于屏幕旋转这类频繁的配置变更,就显得有些笨重了。

现代化方案:ViewModel + SavedStateHandle

所以我们会使用 Jetpack ViewModel。

ViewModel 是一个专为存储和管理 UI 相关数据而设计的类。它能在配置变更(如屏幕旋转)中存活下来。当 Activity 被销毁并重新创建时,新的 Activity 实例会得到同一个 ViewModel 实例,数据就能保留下来。

该怎么选择呢?

如果只是想在屏幕旋转时,保存数据,就无脑使用 ViewModel

而又要在进程被系统杀死后,保存数据,就需要 ViewModel + SavedStateHandle 配合使用,SavedStateHandle 本质上是对 onSaveInstanceState 机制的封装,我们可以使用它来保存那些必须在进程终止后也能恢复的关键数据。