安卓基础知识之Activity篇(二):Activity四大启动模式

安卓基础知识系列旨在提供面试或工作中常用的基础知识,让对安卓还不太熟悉的小伙伴更快地入门。同时自己在工作中,也没法完全记住所有的基础细节,写一篇这样系列,可以给自己日后留个知识参考。

开始的开始

安卓四大组件中,最常用的组件莫过于我们的 Activity 组件。安卓程序员每天都在直接或间接地接触着 Activity,所以 Activity 基础知识的重要性不言而喻。

正文

Activity有四种启动模式:

  1. Standard:标准模式
  2. SingleTop:栈顶复用模式
  3. SingleTask:栈内复用模式
  4. SingleInstance:单实例模式

任务栈

在分别描述四种启动模式的特性前,需先引出一个概念:任务栈(也可以叫返回栈),它是一种后入先出的数据结构。

每个Activity都依附着任务栈运行,系统在创建Activity前会先创建一个ActivityRecord对象,用于映射该Activity,并将ActivityRecord对象压入任务栈中并处于栈顶,然后再通过ActivityRecord对象生成Activity实例。每当我们按下Back键或调用finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈,前一个入栈的Activity就会重新处于栈顶的位置。系统总会显示处于栈顶的Activity给用户。

一部正在运行app的安卓手机中,会有一个前台任务栈和0个或多个后台任务栈。当前在手机上运行的窗口,会有一个前台的任务栈,而处于后台的任务列表,每一个都对应着一个后台任务栈。当用户将后台应用重新切换至前台时,也伴随着将当前运行的前台任务栈移至后台,将系统下一秒想要运行的后台任务栈切换至前台运行的过程。

安卓基础知识之Activity篇(二):Activity四大启动模式

可以通过配置的方式指定Activity想要依附的任务栈:

<activity android:name=".MainActivity"
        android:taskAffinity="com.jamgu.test">
</activity>

通过在AndroidManifest.xml文件中设置,给MainActivity设置想要的任务栈是名为“com.jamgu.test”的任务栈。如果不设置,Activity默认所需任务栈的名字为程序的包名,即默认会将MainActivity压入与程序包名相同的任务栈。

taskAffinity属性主要与 singleTask 启动模式或 allowTaskReparenting 属性配对使用,其他情况下没有意义。

值得注意的是,任务栈实际上也是一个对象(TaskRecord),任务栈之间可以有相同的名字,但实际上如果它们是不同的对象,那么也就不是同一个任务栈。在后文聊到 SingleInstance 启动模式时,再举例说明,这里先简单提一下。

Standard:标准模式

这是系统默认的Activity启动模式,每一次启动一个新的Standard模式的Activity,系统都会创建一个新的Activity实例,无论它在任务栈中是否已经有实例。在这种模式下,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity所在的任务栈中。**比如 Activity A 启动了 Activity B (B 是标准模式),那么 B 就会进入 A 所在的任务栈中。**每创建一个新的实例,就会走一遍Activity的经典生命周期。

SingleTop:栈顶复用模式

顾名思义,当 Activity 的启动模式为 SingleTop 时,在启动 Activity 时如果发现任务栈的栈顶已经是该 Activity 的实例,则认为可以直接复用这个实例,不会创建新的Activity实例。此时,Activity 的 onCreate、onStart 不会被系统调用,而另一个方法 onNewIntent 会被调,调用完 onNewIntent 后,紧接着会调用 onResume 方法。

如果新 Activity 的实例在栈内已经存在,但不位于栈顶,那依然会重新创建一个 Activity 实例,并放入栈顶。比如,如果此时任务栈内的实例顺序是ABC,C 位于栈底且是 SingleTop 模式,此时再次启动一个 C,那么此时栈内实例顺序为CABC。

与 Standard 标准模式一样,谁启动了 SingleTop 模式的Activity,那么这个Activity就会运行在启动它的那个Activity所在的任务栈中。

SingleTask:栈内复用模式

这是一种单实例模式,在这种模式下,只要 Activity 在一个栈中存在,那么多次启动此 Activity 都不会重新创建实例,和 singleTop 一样,复用时系统会回调其 onNewIntent 方法。

一个具有 SingleTask 模式的 Activity 在请求启动后,系统会先寻找是否有该 Activity 想要的任务栈,如果没有,系统则会先创建一个该 Activity 想要的任务栈,然后创建该 Activity 的实例,并放入栈顶。

反之,如果事先已经有了这样一个任务栈,系统则会看看该任务栈是否存在该 Activity 的实例,如果没有该 Activity 的实例,系统也会重新创建一个并放入栈顶。如果已经有该实例,系统则会把该实例调到栈顶,将在该实例之上的其他 Activity 出栈(如果有),并调用该实例的 onNewIntent 方法。

安卓基础知识之Activity篇(二):Activity四大启动模式

图中,右边的详细过程,因为 B 位于栈顶时,A 此时位于后台,处于停止状态,所以当从 B 跳到 A 时,会依次执行 B.onPause()、A.onNewIntent()、A.onRestart()、A.onStart()、A.onResume()、B.onStop()、B.onDestroy()。

如果实例在栈顶被复用,自身执行的生命周期是:onPause()、onNewInetnt()、onResume()。与 SingleTop 启动模式相同。

如果实例在栈内被复用,自身执行的生命周期是:onNewIntent()、onRestart()、onStart()、onResume()。因为实例处于栈内,不在前台,Activity 处于暂停状态,所以会有 onRestart(),onStart() 过程。

SingleInstance:单实例模式

这是一种加强的SingleTask模式,它除了具有 singleTask 模式的所有特性外,还加强了一点,那就是具有此模式的 Activity 只能单独位于一个任务栈中。

同时,SingleInstance 模式也可以与 taskAffinity 配对使用,通过 taskAffinity 指定 Activity 想要依附的任务栈名字。

考虑一种情况,两个Activity:A,B 都指定为 SingleInstance 启动模式,并同时指定 A,B 的 taskAffinity 为 com.jamgu.test1,即为它们指定相同的任务栈栈名。结合 SingleInstance 的特性:具有此模式的 Activity 只能单独位于一个任务栈中。那么这两个 A,B 指定了相同任务栈名字的 Activity ,同时打开时会发生什么情况呢,它们会争同一个任务栈吗?它们会因为争同一个任务栈而杀掉对方以维护自己的”独立存在“特性吗?

举个例子看看,设置 AndroidManifest.xml:

<activity android:name=".MainActivity2"
        android:launchMode="singleInstance"
        android:taskAffinity="com.jamgu.test1"/>
<activity android:name=".dragsort.Drag2SortDemoActivity"/>
<activity android:name=".MainActivity"
        android:launchMode="singleInstance"
        android:taskAffinity="com.jamgu.test1">
   ...
</activity>

先打开 MainActivity,再通过 MainActivity 打开 MainActivity2,看看任务栈的情况。

在命令行,输入一下命令,打印 activity 相关信息

adb shell dumpsys activity

安卓基础知识之Activity篇(二):Activity四大启动模式

只看任务栈的信息,可以看到,虽然 MainActivity 与MainActivity2 有相同的任务栈名,但系统实际上还是为它们创建了不同的任务栈实例,并分别将它们单独放到各自 taskAffinity 指定的任务栈中,两者互不冲突。

回顾一下之前介绍任务栈时提到的:任务栈实际上也是一个对象(TaskRecord),任务栈之间可以有相同的名字,但实际上如果它们是不同的对象,那么也就不是同一个任务栈。

所以,上面的问题显然有了答案,那就是不会。

启动模式的一般使用场景
  1. standard 标准模式

    默认的启动模式,适用于应用的大多数场景。

  2. singleTop 栈顶复用模式

    为了满足特定功能,不需要重复创建显示的页面,比如登录页面,从通知栏消息打开的页面。

  3. singleTask 栈内复用模式

    适用于重复创建比较耗性能的页面,比如主页面,WebView页面(WebView是一个很耗性能的组件)。

  4. singleInstance 单实例模式

    这个启动模式我们一般用不到,系统级应用使用该模式比较多。比如锁屏页面,来电显示页面等。

上面提到的场景只是一般的使用场景,具体哪个页面使用哪种启动模式,只需要根据每种启动模式的特性以及你的业务场景和性能消耗来决定就好。

与启动模式相关的Flags

Activity的启动模式,不仅可以通过 AndroidManifest.xml 文件来指定,也可以通过在 Intent 中指定 FLAGS 来设置 Activity 启动时的行为。

val intent = Intent()
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
mContext?.startActivity(intent)

Intent 中的标志位有很多,这里就介绍几个与启动模式相关的常用flags。

FLAG_ACTIVITY_SINGLE_TOP

相当于给 Activity 设置了 singleTop 的启动模式。

FLAG_ACTIVITY_NEW_TASK

给 Intent 设置这个 flag 会有两种情况:

  • 即将启动的 Activity 所想要依附的任务栈存在时:

    则按该 Activity 在 AndroidManifest.xml 文件中设置的启动模式进行下一步。

    若在 xml 文件中设置的启动模式为 standard,那么每次打开都会重新创建一个 Activity 实例。

    若在 xml 文件中设置 singleTask,则会判断任务栈中是否有该 Activity 来决定是否复用,有的话则会直接复用,并调用其 onNewIntent() 方法。若没有则会重新创建一个实例。

    其他的启动模式同理,按着上面讨论的启动模式特性来理解就可以了。

    若在 xml 文件中没有设置 launchMode,就按默认的 standard 启动模式启动,每次都会重新创建一个新的 Activity 实例。

  • 不存在时:如果待启动的 Activity 设置了 taskAffinity 值,则会先创建一个 taskAffinity 为名的任务栈,然后按待启动 Activity 设置的启动模式启动该 Activity。若没有设置 taskAffinity 属性,则按程序的包名创建一个任务栈,同样以 Activity 设置的启动模式启动 Activity。

总结地说,FLAG_ACTIVITY_NEW_TASK 会帮 Activity 管理其任务栈:如果所需任务栈已经存在了,则设置这个 FLAG 标志位不会有其他任何效果。如果所需任务栈不存在,则会根据所需任务栈的名字创建一个任务栈。任务栈名可以通过 taskAffinity 属性指定。

安卓开发者官网上提供的关于FLAG_ACTIVITY_NEW_TASK标志位的描述并不详细。现网上也流传着设置 FLAG_ACTIVITY_NEW_TASK 就等于在 xml 文件设置 launchMode 为 SingleTask 的说法,这种笼统地说法也是不准确的。

以上的总结,经过我在虚拟机上验证过了,是没问题的,算是补全了对该标志位的描述。

安卓中我们经常通过 Context 对象来启动 Activity,它是一个上下文对象,里面包含了我们建立一个上下文的各种配置信息。这个上下文,可以是一整个应用(Application),可以是一个页面(Activity),也可以是一个服务(Service)。

所以,Context 这个对象有三种类型,分别是 Application ContextService ContextActivity Context。这三种类型的 context 对象都可以用来启动 Activity。

Activity Context 启动 Activity 就不用说了,我们经常使用它。但是你试过用Application Context 和 Service Context 去启动一个 Activity 吗?

一些同学可能试过,第一次使用除 Activity Context 以外的 Context 对象来启动页面时,都会遇上一个报错:

"Calling startActivities() from outside of an Activity "
                    + " context requires the FLAG_ACTIVITY_NEW_TASK flag on first Intent."
                    + " Is this really what you want?"

这是因为只有 Activity 才有任务栈的概念,Application 和 Service 是没有任务栈的概念的,所以我们用 Application Context 或 Service Context 去启动一个Activity的时候,都需要显示地加上 FLAG_ACTIVITY_NEW_TASK 这个标志位,系统才会给这个 Activity 创建任务栈并启动。

FLAG_ACTIVITY_CLEAR_TOP

如果要启动的 Activity 已经在当前任务中运行,则不会启动该 Activity 的新实例,而是会销毁位于它之上的所有其他 Activity,并通过 onNewIntent() 将此 intent 传送给它的已恢复实例(现在位于堆栈顶部)。

这个标志位经常与 FLAG_ACTIVITY_NEW_TASK 结合使用,它俩结合使用时的效果,实际上就是同时设置 FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_CLEAR_TOP 标志位时的效果。正所谓,听君一席话,如听一席话,哈哈,不过我没在开玩笑,它俩结合使用的效果,真的是如上所说的那样!

不过,如果被启动的 Activity 采用 standard 模式启动,那么它连同它之上的 Activity 都要出栈,系统会创建新的 Activity 实例并压入栈顶。

文章内容参考
  1. 安卓开发者官网
  2. 安卓开发艺术探索,任玉刚

最后的最后

安卓启动模式知识就到这里啦,期待看到这里的小伙伴有个不错的收获,咱们下一篇文章再见~

兄dei,如果觉得我写的还不错,麻烦帮个忙呗

上一篇:设计一种需要登录后跳转到目标Activity的方案


下一篇:Android - 一次性解决 Manifest merger failed : Apps targeting Android 12 and higher are required to ... 问题