Activity的生命周期和启动模式
作为Android四大组件之中存在感最强的组件,Activity应该是我们在学习Android中第一个碰到的新概念。在日常开发过程中我们肯定会用到Activity,但是关于Activity的一些细节问题运行机制我们可能还有一些不清楚的问题。今天我们就来详细说明Activity的生命周期和启动模式。
内容概要
在本篇文章中,将会介绍以下比较具体的内容:
- context对象是什么
- 什么是失去焦点和不可见
- Activity的生命周期
- 异常状态下该如何处理Activity的重新创建
- 什么是任务和任务栈
- 五种启动模式和任务亲和性
- 清除返回堆栈
Activity的生命周期
1. 什么是Activity的生命周期和上下文对象Context?
什么是Activity的生命周期?
简单来说,Activity的生命周期是指一个Activity从其从创建到销毁的过程中会处于不同的状态,每个状态也有其对应的回调方法,这些回调会让 Activity 知晓某个状态已经更改:系统正在创建、停止或恢复某个 Activity,或者正在销毁该 Activity 所在的进程。
上下文对象Context是什么?
这里为了帮助我们理解生命周期的意义,我觉得有必要简单介绍一下Context。
我们有没有考虑过一个问题,为什么我们在启动Activity时不能直接New 一个Activity使用,而是需要先创建一个Activity类然后用Intent配合startActivity启动呢?说到底这是因为Activity(包括Android中的许多其他组件)是一个系统级的组件,它不能单独出来运行,而是需要依赖于Android系统来运行和进行交互,因此我们在创建它的时候就需要根据当前的Android系统的环境来进行配置,具体来说包括你的手机屏幕大小等参数。
那该如何获取当前Android系统环境下的各种参数呢?这时候就需要用到Context类了,我们先看看Google官方的介绍:
Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.
简单来说,Context是有关应用程序环境的全局信息的接口。它允许访问特定于应用程序的资源和类,以及对应用程序级操作(如启动活动、广播和接收意图等)的向上调用。说到底,startActivity这个方法也是在Context接口中定义的。许多系统级的操作基本都要用到Context对象。
我觉得这也是生命周期存在的意义:让Activity与Android更好地交互
2.在Activity的生命周期中跳转
什么是失去焦点和不可见?
我们经常会在文档中看到失去焦点和应用不可见等表述,其实简单来说(对于Activity来说),失去焦点就是指Activity不在前台和我们交互了但是我们仍可以看间这个Activity,比如说我们在切换应用时的窗口:
这里我们仍可以看到我们的QQ音乐APP中的页面,这就代表它仍然可见,但是此时我们并没有和QQ音乐APP在进行交互,这就叫做QQ音乐的Activity失去了焦点。那不可见的意思就是我们完全看不到QQ音乐APP的Activity的页面了。
具体的七个回调方法
Activity在一般情况下会执行以下七个回调方法
-
onCreate():表示Activity正在被创建,必须实现此回调,它会在系统首次创建 Activity 时触发。Activity 会在创建后进入“已创建”状态。
-
onRestart():表示Activity正在重新启动。一般情况下,当当前Activity从不可见新变为可见状态时,onRestart方法就会被调用。
-
onStart():表示Activity正在被启动,即将开始,此时Activity已经可见了,但是还没有出现在前台和我们进行交互。可以理解为Activity已经显示出来了但是我们还看不到。从官网的资料来说:当 Activity 进入“已开始”状态时,系统会调用此回调。onStart() 调用使 Activity 对用户可见,因为应用会为 Activity 进入前台并支持互动做准备。例如,应用通过此方法来初始化维护界面的代码。
-
onResume():表示Activity已经可见了,并且出现在前台并开始活动。这是应用与用户互动的状态。应用会一直保持这种状态,直到某些事件发生,让焦点远离应用。此类事件包括接到来电、用户导航到另一个 Activity,或设备屏幕关闭。
-
onPause():表示Activity正在停止,正常情况下,onStop回调将紧接着被执行。系统将此方法视为用户将要离开 Activity 的第一个标志(尽管这并不总是意味着 Activity 会被销毁);此方法表示 Activity 不再位于前台(尽管在用户处于多窗口模式时 Activity 仍然可见)。
需要额外提醒的是:我们最好不要在onPause方法中执行太过耗时的操作,因为只有onPause执行完后,新Activity的onResume方法才会执行。如果我们要释放资源的话,最好还是在onDestroy中给执行。
-
onStop():表示Activity即将停止,可以做一些稍微重量级级的回收工作。在 onStop() 方法中,应用应释放或调整在应用对用户不可见时的无用资源。例如,应用可以暂停动画效果,或从精确位置更新切换到粗略位置更新。
-
onDestroy():表示Activity即将被销毁,这是Activity生命周期中的最后一个回调,在这里我们可以做一些回收工作和最终资源的释放。
下图是我从Google官网翻译而来的流程图,我标注出了Activity仍然可见的部分:
这七个回调中明显有三个配对:
- onCreate – onDestroy 从Activity创建与销毁的角度来说
- onStart – onStop 从Activity的启动与停止的角度来说
- onResume – onPause 从Activity是否获得了焦点的角度来说
3.特殊情况下生命周期的跳转和处理
除了用户正常交互状态下Activity的生命周期会发生变化以外,还有一些特殊情况也会导致Activity生命周期发生变化。这与我们在探讨Context中的内容有联系,由于Activity是依赖于Android环境的,所以当Android环境发生变化时Activity有时也需要发生变化。
内存不足,系统需要释放内存
这种情况就是优先级更高的Activity需要内存但是系统可用的内存不够,系统就会释放掉优先级低的Activity来清出空间。系统永远不会直接终止 Activity 以释放内存,而是会终止 Activity 所在的进程。系统不仅会销毁 Activity,还会销毁在该进程中运行的所有其他内容。 系统终止给定进程的可能性取决于当时进程的状态。反之,进程状态取决于在进程中运行的 Activity 的状态。
下面是官方给出的一张表,展示了进程状态、Activity 状态以及系统终止进程的可能性之间的关系:
系统终止进程的可能性 | 进程状态 | Activity 状态 |
---|---|---|
较小 | 前台(拥有或即将获得焦点) | 已创建 或 已开始 或 已恢复 |
较大 | 后台(失去焦点) | 已暂停 |
最大 | 后台(不可见) | 已停止 |
最大 | 空 | 已销毁 |
系统配置发生了变化
有很多事件会触发配置更改。最显著的例子或许是横屏和竖屏之间的屏幕方向变化。其他情况,如语言或输入设备的改变等,也可能导致配置更改。
当配置发生更改时,Activity 会被销毁并重新创建。原始 Activity 实例将触发 onPause()、onStop() 和 onDestroy() 回调。系统将创建新的 Activity 实例,并触发 onCreate()、onStart() 和 onResume() 回调。
简单来说就是,当发生一些事件,比如说屏幕方向变化时,Activity就会先销毁然后重新进行创建,生命周期如图所示:
这里有两个新出现的回调方法onSaveInstanceState 和 onRestoreInstabceState,前者是用来保存被销毁的Activity的状态的,后者是用来在新创建的Activity中恢复原来的Activity的状态的。
4.处理特殊情况下Activity的重新创建
面对由于系统配置发生变化而重新创建Activity,我们会丢失掉一部分UI界面的信息,这会极大影响用户体验,所以我们需要采取一定的手段来恢复UI的信息或者避免Activity被重新创建。这里我们提供五个方法:
-
使用ViewModel
ViewModel是Google官方提供的一个类,这个类的设计思想遵循MVVM架构,就是视图和数据分离,Activity专注于显示视图,ViewModel专注于处理和保存业务。具体来说,ViewModel可以和Activity进行绑定,只要Activity没有被完全销毁,ViewModel就不会重新创建从而可以避免因为配置改变而导致UI数据丢失。
具体的使用可以查看我的这篇博客➡ViewModel的使用
-
使用onSaveInstanceState方法配合onCreate和onRestoreSavedInstanceState两个方法
我们在上面的那个异常状态下Activity的重建过程的流程图我们可以看到Activity在异常状态下被销毁前会执行onSaveInstanceState方法,这个方法就是用来存储一些数据的。那我们存储好的数据应该在哪里重新读取呢?
一般的话是在onCreate方法或者onRestoreSavedInstanceState。这两个方法都会接受一个Bundle对象,这个Bundle对象里就是存储了之前被销毁前用onSaveInstanceState方法存储的数据。这里给出一个示例:
public class MainActivity extends AppCompatActivity { ActivityMainBinding binding; Integer lastData = new Integer(0); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this,R.layout.activity_main); binding.tvData.setText(String.valueOf(lastData)); binding.btPlus.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { lastData++; binding.tvData.setText(String.valueOf(lastData)); } }); binding.btSubstract.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { lastData--; binding.tvData.setText(String.valueOf(lastData)); } }); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("lastdata",lastData); } @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); lastData = new Integer(savedInstanceState.getInt("lastdata")); binding.tvData.setText(String.valueOf(lastData)); } }
这里我们是在onRestoreInstanceState方法中恢复数据的做法,这也是官方比较推荐的做法。关于在onRestoreInstanceState恢复和onCreate中恢复的区别的话主要是有两个。第一是onRestoreInstanceState方法是在onCreate方法之后执行的;第二是onRestoreInstanceState方法只有在之前已经调用过onSaveInstanceState方法之后才会被执行,而onCreate方法无论如果都会被执行。在具体使用场景体现在onRestoreInstanceState方法中接收的Bundle对象是一定不为空的,而onCreate中的Bundle可能是为null,所以我们在onCreate方法中使用Bundle得先判断一下。
这里我们在给出用onCreate方法恢复数据的示例:
public class MainActivity extends AppCompatActivity { ActivityMainBinding binding; int lastData = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(savedInstanceState != null){ lastData = savedInstanceState.getInt("lastdata",0); } binding = DataBindingUtil.setContentView(this,R.layout.activity_main); binding.tvData.setText(String.valueOf(lastData)); binding.btPlus.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { lastData++; binding.tvData.setText(String.valueOf(lastData)); } }); binding.btSubstract.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { lastData--; binding.tvData.setText(String.valueOf(lastData)); } }); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("lastdata",lastData); } }
-
数据持久化
数据持久化的做法和前面的viewModel思想类似,如果轻量级的数据我们可以用SharedPreference保存,若是复杂的数据我们可以使用Room数据库,Room数据库的用法:
👇
Room数据库的使用这里我们就不详细介绍了。 -
指定configChanges参数
上面三种做法的思想都是保存上一次被销毁的Activity中的数据,而下面做法则是为了避免Activity被重新创建。可以为特定配置更改停用 activity 重新创建。为此,请将配置类型添加到 AndroidManifest.xml 的 < activity > 条目中的 android:configChanges。
下面是一些参数的意义:
值 说明 density 显示密度的更改,例如当用户指定不同的显示比例或其他屏幕当前处于活跃状态时。 fontScale 字体缩放比例的更改,例如当用户选择新的全局字体大小时。 keyboardHidden 键盘无障碍功能的更改,例如当用户显示硬件键盘时。 locale 语言区域的更改,例如当用户选择显示文本所用的新语言时。 orientation 屏幕方向的更改,例如用户旋转设备时。 screenLayout 屏幕布局的更改,例如在其他屏幕变为活动状态时。 screenSize 当前可用屏幕尺寸的更改。 uiMode 界面模式的更改,例如当用户将设备放到桌面或车载基座上时,或者夜间模式发生变化时。 如果需要更多信息可以访问☞官网
这里需要额外提一嘴的就是屏幕方向的改变orientation属性:
注意:如果应用面向 Android 3.2(API 级别 13)或更高版本的系统,则还应声明 “screenLayout” 和 “screenSize” 配置,因为当设备在纵向模式与横向模式之间切换时,屏幕布局和屏幕大小可能会发生变化。
当我们指定了configChanges里的值之后,发生相应的配置变化后将不会导致Activity被重新创建,取而代之的是将会执行onConfigurationChanged的回调方法。onConfigurationChanged() 回调方法会收到一个 Configuration 对象,其中指定了新的设备配置。读取 Configuration 对象中的字段以确定合适的新配置。
注意:需要说明的是虽然官网上说不会重新创建Activity,但是经过我测试UI界面还是会重新加载(就是说由竖屏变横屏时UI界面仍然会重新加载),但是Activity确实没有被重新创建,也就是说UI上的数据确实没有丢失。
-
指定screenOrientation参数 (不推荐)
我们最常遇到的配置更改应该就是屏幕方向的改变了,我们可以设置在manifest清单文件中指定Activity的screenOrientation或者调用setRequestedOrientation设置Activity的屏幕方向,一旦指定了这个参数,那么即使是屏幕方向改变了,Activity也完全不会被销毁。但是这种方法可能也会影响用户体验,所以一般不推荐使用。
Activity的启动模式
什么是任务和任务栈?
在介绍启动模式之前,我们有必要简要介绍一下任务和任务栈的概念。
在 Android 应用程序中,任务(Task)是指一组相关联的 Activity 组成的集合,这些Activity按照每个Activity 打开的顺序排列在一个返回堆栈中。
而任务栈(Task Stack)则是用来保存和管理这些任务的栈。
在 Android 系统中,每个任务栈都有一个相应的状态,例如前台或后台,也就是指当前任务栈所在的进程是否处于用户界面上的最前端,也就是被称为前台。Android 系统只会将一个任务栈置于前台,而其他任务栈则会被置于后台,因此在任意时刻只能有一个任务栈处于前台状态。
当用户切换到另一个应用时,当前应用中的任务栈将被置于后台,而前台将切换到另一个应用程序。用户可以通过按下 Home 键、最近使用的应用程序按钮、强制停止当前应用程序或者调用其他应用程序等方式,来切换前台任务栈。
这里也贴出官方给出的任务的定义:
任务是一个整体单元,当用户开始一个新任务或通过主屏幕按钮进入主屏幕时,任务可移至“后台”。在后台时,任务中的所有 Activity 都会停止,但任务的返回堆栈会保持不变,当其他任务启动时,当前任务只是失去了焦点,如图 2 所示。这样一来,任务就可以返回到“前台”,以便用户可以从他们离开的地方继续操作。举例来说,假设当前任务(任务 A)的堆栈中有 3 个 Activity,当前 Activity 下有 2 个 Activity。用户按主屏幕按钮,然后从应用启动器中启动新应用。主屏幕出现后,任务 A 转到后台。当新应用启动时,系统会启动该应用的任务(任务 B),该任务具有自己的 Activity 堆栈。与该应用互动后,用户再次返回到主屏幕并选择最初启动任务 A 的应用。现在,任务 A 进入前台,其堆栈中的所有三个 Activity 都完好如初,堆栈顶部的 Activity 恢复运行。此时,用户仍可通过以下方式切换到任务 B:转到主屏幕并选择启动该任务的应用图标(或者从最近使用的应用屏幕中选择该应用的任务)。这就是在 Android 上进行多任务处理的一个例子。
提到的图二:
五种启动模式(实际上是5+1)
我们可以在清单文件中声明启动模式,也可以在Intent中指定标志位来声明启动模式。清单文件中我们可以设置launchMode属性,可以指定四种不同的启动模式:
-
standard(默认模式):标准模式。默认的启动模式。每次启动 Activity 都会创建一个新的实例,并放入当前任务栈的栈顶。
-
singleTop(栈顶复用模式):如果新的 Activity 已经位于栈顶,那么不会创建新的实例,而是直接使用现有的实例。如果新的 Activity 不在栈顶,那么会创建新的实例并加入到栈顶。
-
singleTask(栈内复用模式):如果新的 Activity 已经存在于该Activity需要的任务栈中,那么将把位于它之上的所有 Activity 弹出栈顶,使其成为栈顶 Activity,并且不会创建新的实例。如果新的 Activity 不在其需要的任务栈中,那么将创建新的实例并加入任务栈的栈顶。 需要注意的是,这个模式和taskAffinity(任务亲和性)属性密切相关,如果没有指定taskAffinity,那将不会启动新的Task
-
singleInstance(单实例模式):在单独的任务栈中启动 Activity,并且这个任务栈中只会有这个 Activity 的实例。如果已经存在一个实例,那么会直接使用它,而不是创建新的实例。
-
singleInstancePerTask:这个启动模式在官网的中文文档中没有提到,但是实际上是在Android12中新增的。
在使用singleInstancePerTask模式启动一个Activity时,系统会检查当前任务栈中是否已经存在该Activity的实例,如果已经存在,那么该Activity实例所在的任务栈将被移动到前台并恢复,而不是创建新的Activity实例。如果该Activity实例不在当前任务栈中,则会创建新的任务栈,并在该任务栈中创建该Activity实例。
需要注意的是,singleInstancePerTask模式的Activity只能作为任务的根Activity存在,即该Activity是任务的第一个启动的Activity。这是与singleInstance模式不同的地方,singleInstance模式的Activity可以被添加到任意任务栈中。但是与singleTask模式类似,singleInstancePerTask模式的Activity也可以通过FLAG_ACTIVITY_NEW_DOCUMENT和FLAG_ACTIVITY_MULTIPLE_TASK标志位在不同的任务栈中创建多个实例。
如果你使用singleInstancePerTask模式来启动一个Activity,并且该Activity已经存在于同一任务栈中的另一个实例,则会调用该Activity的onNewIntent方法而不是onCreate方法,因为该Activity已经存在于该任务栈中。
而在用startActivity进行启动时,我们可以通过指定Intent的标志位来指定,可以设置三种模式:
-
FLAG_ACTIVITY_NEW_TASK
在新任务中启动 Activity。如果您现在启动的 Activity 已经有任务在运行,则系统会将该任务转到前台并恢复其最后的状态,而 Activity 将在 onNewIntent() 中收到新的 intent。
这与上一节中介绍的 “singleTask” launchMode 值产生的行为相同。
-
FLAG_ACTIVITY_SINGLE_TOP
如果要启动的 Activity 是当前 Activity(即位于返回堆栈顶部的 Activity),则现有实例会收到对 onNewIntent() 的调用,而不会创建 Activity 的新实例。
这与上一节中介绍的 “singleTop” launchMode 值产生的行为相同。 -
FLAG_ACTIVITY_CLEAR_TOP
如果要启动的 Activity 已经在当前任务中运行,则不会启动该 Activity 的新实例,而是会销毁位于它之上的所有其他 Activity,并通过 onNewIntent() 将此 intent 传送给它的已恢复实例(现在位于堆栈顶部)。
launchMode 属性没有可产生此行为的值。但是需要说明的是 singleTask模式默认就有clearTop的效果FLAG_ACTIVITY_CLEAR_TOP 最常与 FLAG_ACTIVITY_NEW_TASK 结合使用。将这两个标记结合使用,可以查找其他任务中的现有 Activity,并将其置于能够响应 intent 的位置。如果指定 Activity 的启动模式为 “standard”,系统也会将其从堆栈中移除,并在它的位置启动一个新实例来处理传入的 intent。这是因为当启动模式为 “standard” 时,始终会为新 intent 创建新的实例。
任务亲和性(TaskAffinity)
上面的SingleTask启动模式提到了Activity需要的任务栈,那什么是Activity需要的任务栈呢?实际上这和任务亲和性有关,“亲和性”表示 Activity 倾向于属于哪个任务。默认情况下,同一应用中的所有 Activity 彼此具有亲和性。因此,在默认情况下,同一应用中的所有 Activity 都倾向于位于同一任务。但是我们也可以指定任务亲和性:
<activity
android:name=".SecondActivity"
android:launchMode="singleTask"
android:taskAffinity="my.task"
android:exported="false" />
需要额外说明的是,我们自定义的taskAffinity的名称也有一定的命名规范,taskAffinity字符串的值至少需要包含一个小数点,也就是说,最简单的形式:“xx.xx”。而且该值必须不同于manifest元素中声明的默认软件包名称。
亲和性会在两种情况下发挥作用:
-
当启动 Activity 的 intent 包含 FLAG_ACTIVITY_NEW_TASK 标记时。
默认情况下,新 Activity 会启动到调用 startActivity() 的 Activity 的任务中。它会被推送到调用方 Activity 所在的返回堆栈中。但是,如果传递给 startActivity() 的 intent 包含 FLAG_ACTIVITY_NEW_TASK 标记,则系统会寻找其他任务来容纳新 Activity。通常会是一个新任务,但也可能不是。如果已存在与新 Activity 具有相同亲和性的现有任务,则会将 Activity 启动到该任务中。如果不存在,则会启动一个新任务。关于额外的信息,官网有说到:如果此标记导致 Activity 启动一个新任务,而用户按下主屏幕按钮离开该任务,则必须为用户提供某种方式来返回到该任务。有些实体(例如通知管理器)总是在外部任务中启动 Activity,而不在它们自己的任务中启动,因此它们总是将 FLAG_ACTIVITY_NEW_TASK 添加到传递给 startActivity() 的 intent 中。如果您的 Activity 可由外部实体调用,而该实体可能使用此标记,请注意用户可以通过一种独立的方式返回到所启动的任务,例如使用启动器图标
-
当TaskAffinity 和 allowTaskReparenting 配对使用时。
当一个应用A启动了应用B的某个Activity后,如果这个Activity的allowTaskReparenting属性为true的话,那么当应用B被启动后,此Activity会直接从应用A的任务栈转移到B的任务栈中。举例来说,假设一款旅行应用中定义了一个报告特定城市天气状况的 Activity。该 Activity 与同一应用中的其他 Activity 具有相同的亲和性(默认应用亲和性),并通过此属性支持重新归属。当您的某个 Activity 启动该天气预报 Activity 时,该天气预报 Activity 最初会和您的 Activity 同属于一个任务。不过,当旅行应用的任务进入前台运行时,该天气预报 Activity 就会被重新分配给该任务并显示在其中。
这样可能还是比较抽象,这里借由开发艺术探索里的例子:比如现在有两个应用A和B,A启动了B的一个ActivityC,然后按Home键回到桌面,然后再单击B的桌面图标,这个时候并不是启动了B的主Activity,而是重新显示了已经被应用A启动的ActivityC,或者说,C从A的任务栈转移到了B的任务栈中。可以这么理解,由于A启动了C,这个时候C只能运行在A的任务栈中,但是C属于B应用,正常情况下,它的TaskAffinity值肯定不可能和A的任务栈相同。所以,当B被启动后,B会创建自己的任务栈,这个时候系统发现C原本所想要的任务栈已经被创建了,所以就把C从A的任务栈中转移过来了,这也是该属性名为“重新归属性”的原因吧。
清除返回堆栈
如果用户离开任务较长时间,系统会清除任务中除根 Activity 以外的所有 Activity。当用户再次返回到该任务时,只有根 Activity 会恢复。系统之所以采取这种行为方式是因为,经过一段时间后,用户可能已经放弃了之前执行的操作,现在返回任务是为了开始某项新的操作。
当然我们也可以通过修改一些属性来修改:
- alwaysRetainTaskState
如果在任务的根 Activity 中将该属性设为 “true”,则不会发生上述默认行为。即使经过很长一段时间后,任务仍会在其堆栈中保留所有 Activity。 - clearTaskOnLaunch
如果在任务的根 Activity 中将该属性设为 “true”,那么只要用户离开任务再返回,堆栈就会被清除到只剩根 Activity。也就是说,它与 alwaysRetainTaskState 正好相反。用户始终会返回到任务的初始状态,即便只是短暂离开任务也是如此。 - finishOnTaskLaunch
该属性与 clearTaskOnLaunch 类似,但它只会作用于单个 Activity 而非整个任务。它还可导致任何 Activity 消失,包括根 Activity。如果将该属性设为 “true”,则 Activity 仅在当前会话中归属于任务。如果用户离开任务再返回,则该任务将不再存在。