Activity 启动模式

719 阅读13分钟

Activity 启动模式是一个面试中常考的知识点,但很多的开发者在面试时都倒在了这个知识点上。

很多的开发者都只是简单了解 LaunchMode 的四个属性值,但是 Activity 启动模式在不同场景下具有不同的效果,这不仅仅是四个简单的属性值能够涵盖的。

通过这篇文章,我们就来梳理一下被称为 "面试黑洞" 的 Activity 启动模式。

Task 究竟是什么?

在 Android 的系统栏中,有三个功能键:Back(返回)、Home(显示桌面)和 Menu(打开 Task 视图)

QQ截图20210718215125.png

当我们按下 Menu 键后,就会将当前显示的 Activity 从前台转入后台,打开 Task 视图,展示系统中所有存在的活动栈(活动栈就是 Task)

Task 又被称为任务栈和返回栈,这是基于不同角度给出的定义,但本质上任务栈和返回栈指的都是同一个东西。

作为 Android 开发者,我们都知道在 App 中启动的 Activity 都会被放入 Task 进行管理。但是有很多的开发者都误以为 Task 是归属于 App 管理的,认为 Task 视图中展示的其实是 App。

这种想法是错误的。

首先,Task 归属于 Android 系统管理。对于 Task,没有线程、进程的概念,它不归属于 App,它仅仅是 Android 系统中用于管理已经启动的 Activity 的一个容器。

Task 本质上是一系列 Activity 的集合。Task 按照栈的数据结构对其中的 Activity 进行管理。如果 Task 处于前台,那么处于 Task 底部(相当于栈顶)的 Activity 会被展示到界面上,执行对应的生命周期函数。

其次,Task 视图中展示的 Task 是按照 Affinity 进行分类的。也就是说,Task 视图中展示的 Task 的 Affinity 不相同。

关于 Activity 的启动

在 App 中,Activity 的所有行为可以分为两种:

  • 新建(Activity 启动模式只作用于这种行为)
  • 恢复

在 App 中,Activity 的大多数行为都是新建(通过 startActivity 函数启动 Activity,例如:直接调用、点击通知等)。因此我们只需要知道常见的恢复行为,就能区分 App 中所有常见的 Activity 行为了。

最常见的恢复行为就是通过 Back 键返回当前 Task 中的上一个 Activity。当按下 Back 键之后,会将当前位于 Task 底部的 Activity 从 Task 中移除并销毁;此时上一个 Activity 就重新位于 Task 底部,显示到界面上。

在 Task 视图中通过点击 Task 使得对应的 Task 返回前台,处于该 Task 底部的 Activity 会显示到界面上。这通常是一种恢复行为。

为什么说是通常呢?因为在 Task 视图中显示的 Task 不一定都是存活的。当用户不断地通过点击 Back 键返回上一个 Activity,当 Task 中没有其他 Activity 时,再点击 Back 键就会返回桌面,此时这个空的 Task 就会被销毁。

但如果点击 Menu 键打开 Task 视图,就会发现这个被销毁的 Task 仍处于 Task 视图中,这是为何?

这其实是 Android 系统基于用户考虑的一种处理,可以理解为此时的这个 Task 是一个最近使用过的记录。当用户再次点击这个 Task 时,就会重新启动这个 Task 的 Affinity 值所对应的 App。

同样的在 Launcher 中通过点击 App 应用图标来启动 Activity 有可能也是恢复行为。

通过点击 App 应用图标来启动 Activity 是否属于恢复行为取决于所启动 App 的 Affinity 值所对应的 Task 是否存活。如果对应的 Task 存活,则将该 Task 返回前台,显示处于该 Task 底部的 Activity,这就属于恢复行为;如果对应的 Task 已经被销毁,则重新启动这个 Task 对应的 App,这就属于新建行为。

Affinity 是什么?

Affinity 译为亲和性,用于表示 Activity 倾向于将自身存放在哪个 Task 中。可以在 AndroidManifest 文件中通过 android:taskAffinity 属性进行声明。

默认情况下,如果不主动通过 android:taskAffinity 属性进行声明,Activity 默认会以当前应用的包名作为自己 android:taskAffinity 的属性值

从概念上讲,具有相同 Affinity 的 Activity 应该归属于同一个 Task(实际上并不一定);从用户的角度来看,则应该归属于同一个 App。

因为每种 Affinity 在 Task 视图中会各自独占一个列表项,看起来就像一个个单独的 App。而实际上一个列表项中的 Activity 可能来自于多个不同的 App。

Task 的 Affinity 值由被放入的第一个 Activity(栈底的 Activity)决定,且不会再更改

Activity 启动模式

Activity 启动模式是一个很复杂的难点,其决定了要启动的 Activity 和 Task 之间的关联关系,直接影响到了用户的直观感受。

Activity 启动模式可以通过以下两种方式来进行定义:

  • 在 AndroidManifest.xml 文件中为 Activity 设置 android:launchMode 属性值。
  • 当调用 startActivity(Intent) 函数启动 Activity 时,向 Intent 添加或设置 flag 标记位。

可在实际业务场景中,往往需要 LaunchMode 和 Intent flag 搭配使用,同时还需要考虑多应用交互,具体情况就会变得复杂很多。

其复杂性和难点主要就在于:

  • Task 中保存的 Activity 可能来自于不同的 App;一个 App 中也可能包含多个 Task。
  • Task 之间可以进行堆叠。
  • 已经处于某一个 Task 中的 Activity 可以被迁移到另外一个 Task 中。
  • Intent flag 的属性值可以进行多组合使用。

有些启动模式可通过 LaunchMode 来定义,但不能通过 Intent flag 定义;同样,有些启动模式可通过 Intent flag 定义,却不能在 LaunchMode 中定义。两者互相补充,但不能完全互相替代,且 Intent flag 的优先级会更高一些

LaunchMode

LaunchMode 通过 AndroidManifest.xml 文件静态定义,只有四种属性值:

  • 标准模式(standard)
  • 栈顶复用模式(singleTop)
  • 全局单例模式(singleTask)
  • 单实例模式(singleInstance)

标准模式(standard)

standard 是 LaunchMode 的默认值。如果我们没有在 AndroidManifest.xml 文件中为 Activity 主动设置 android:launchMode 属性值,则 Activity 的 android:launchMode 属性值默认为 standard。

standard 的作用效果:每次 Activity 启动,都会新建一个 Activity 实例,并将新建的 Activity 实例放入到启动者所在的 Task 中

栈顶复用模式(singleTop)

singleTop 的作用效果:启动 Activity 时,如果目标 Activity 与启动者 Activity 相同,且启动者 Activity 正位于当前 Task 的底部(栈顶),就会重用启动者实例,并回调该实例的 onNewIntent 函数;否则新建一个 Activity 实例,并将新建的 Activity 实例放入到启动者所在的 Task 中

全局单例模式(singleTask)

singleTask 的作用效果:启动 Activity 时,如果目标 Activity 实例已经存在,就会重用该实例,回调该实例的 onNewIntent 函数;否则新建一个 Activity 实例,并将新建的 Activity 实例放入到 Affinity 所对应的 Task 中

在前面介绍 standard 和 singleTop 的作用效果时,都是基于启动者所在的 Task 来说明的。

那如果给设置了 standard / singleTop 的 Activity 指定与启动者所在的 Task 不同的 Affinity 呢?

结果就是:不会有任何变化!

设置了 standard / singleTop 的 Activity 仍只会与启动者所在的 Task 关联。换句话说就是,Affinity 对于设置了 standard / singleTop 的 Activity 来说没有任何作用

设置了 singleTask 的 Activity,只会与 Affinity 所对应的 Task 关联

举个例子:

假设 Activity A 和 Activity B 都设置了 singleTask;

Activity A 的 Affinity 是 com.example.test,Activity B 的 Affinity 是 com.example.test.other

启动包名为 com.example.test 的 App,会默认启动 Activity A,新建一个 Affinity 是 com.example.test 的 Task;

此时如果再启动 Activity B,就会再新建一个 Affinity 是 com.example.test.other 的 Task,并将 Activity B 的实例放入该 Task。

Task 的堆叠

对于以上例子,有一个细节:

当启动了 Activity B 后,就会将 Affinity 是 com.example.test.other 的 Task 压在 Affinity 是 com.example.test 的 Task 之上,进行堆叠;

如果此时在 Activity B 中又启动 Activity A,就会复用处于 Affinity 是 com.example.test 的 Task 中的 Activity A 实例,并将 Affinity 是 com.example.test 的 Task 压在 Affinity 是 com.example.test.other 的 Task 之上,进行堆叠。

当启动 Activity 时,如果需要切换到别的 Task,则系统会将目标 Task 压在启动者所在的 Task 之上,使其成为前台。就如同示例里描述的一样。

如果用户不断通过点击 Back 键进行退出操作,就会看到很明显的切换 Task 的动画效果。当用户不断点击 Back 键进行退出操作时,会先将堆叠在最上方的 Task 中的 Activity 依次移出;当最上方的 Task 中没有 Activity 时就会被销毁,然后继续移出下一个 Task 中的 Activity。

前面我们说过,Task 视图中展示的列表项是按 Affinity 分类的。当我们点击 Menu 键打开 Task 视图时,会将所有的 Task 移至后台,并按照 Affinity 分类进行展示。

也就是说,当打开 Task 视图时,会将所有堆叠起来的 Task 进行拆分

单实例模式(singleInstance)

singleInstance 的作用效果:启动 Activity 时,如果该 Activity 实例已经存在,就会重用该实例,回调该实例的 onNewIntent 函数;否则新建一个 Task 和 Activity 实例,并将新建的 Activity 实例放入到新建的 Task 中

singleInstance 与 singleTask 的作用效果很像,但却有些许不同。

对于 singleTask 而言,当不满足重用条件时,会全局寻找 Affinity 所对应的 Task(没有就新建),将新建的 Activity 实例放入其中;

而对于 singleInstance 而言,当不满足重用条件时,一定会新建一个 Task 用来存放新建的 Activity 实例,无论 Affinity 是什么

singleInstance 可以理解为是 singleTask 的 "加强版"。

singleTask 要求的是对于系统中只能有一个自己的 Activity 实例(唯一性),而 singleInstance 在 singleTask 要求的基础之上还要求独占一个 Task(唯一性 + 独占性)。

Intent flag

在启动 Activity 时,我们可以通过在传送给 startActivity(Intent) 函数的 Intent 中设置一个或多个相应的 flag 来修改 Activity 与其 Task 的默认关联(LaunchMode),即 Intent flag 的优先级比 LaunchMode 高。

Intent flag 有很多个,比较常用的有四个:

  • Intent.FLAG_ACTIVITY_NEW_TASK
  • Intent.FLAG_ACTIVITY_CLEAR_TASK
  • Intent.FLAG_ACTIVITY_SINGLE_TOP
  • Intent.FLAG_ACTIVITY_CLEAR_TOP

Intent.FLAG_ACTIVITY_NEW_TASK

Intent.FLAG_ACTIVITY_NEW_TASK 应该是大多数开发者最熟悉的一个 flag,最常用的一个场景就是用于在非 ActivityContext 的情况下启动 Activity。

Intent.FLAG_ACTIVITY_NEW_TASK 的作用可以理解为是激活 Affinity,根据 Affinity 决定是否要切换 Task 来存放目标 Activity

前面我们说过,Affinity 对于设置了 standard / singleTop 的 Activity 来说没有任何作用,但这个结论也只适用于没有添加 Intent.FLAG_ACTIVITY_NEW_TASK 的情况。

Intent.FLAG_ACTIVITY_NEW_TASK 对于设置了 singleTask / singleInstance 的 Activity 来说没有任何作用

Intent.FLAG_ACTIVITY_NEW_TASK 对于 standard 和 singleTop 的作用效果会有些许不同,我们需要分类型讨论。

对于 standard,当目标 Activity 的 Affinity 与启动者所在 Task 的 Affinity 不一致时,就会切换 Task 来存放目标 Activity

而对于 singleTop,相当于在 standard 的基础上多加了一个限制,只有当目标 Activity 的实例不位于启动者所在 Task 的底部(栈顶)且目标 Activity 的 Affinity 与启动者所在 Task 的 Affinity 不一致时,才会切换 Task 来存放目标 Activity

在这里只分析了是否切换 Task 的条件,对于切换或不切换 Task 后是否新建目标 Activity 实例、是否重用已经存在的实例等情况,逻辑会过于复杂,建议在实际开发过程中单独进行测试来查看效果。

Intent.FLAG_ACTIVITY_CLEAR_TASK

Intent.FLAG_ACTIVITY_CLEAR_TASK 的源码注释标明了该 flag 必须和 Intent.FLAG_ACTIVITY_NEW_TASK 组合使用。

此标志将导致在 Activity 启动之前清除与该 Activity 关联的任何已经存在的 Task。

也就是说,该 Activity 将成为其他空 Task 的新根,所有的旧 Activity 都将被移除。

这只能与 FLAG_ACTIVITY_NEW_TASK 一起使用。

单独使用 Intent.FLAG_ACTIVITY_CLEAR_TASK 是没有任何效果的,一定要配合 Intent.FLAG_ACTIVITY_NEW_TASK 一起使用。

Intent.FLAG_ACTIVITY_CLEAR_TASK + Intent.FLAG_ACTIVITY_NEW_TASK 组合起来的作用效果是:

将 Affinity 所对应 Task 作为目标 Task

如果目标 Task 存在,便将目标 Task 中所有 Activity 移除,然后新建一个目标 Activity 实例放入到目标 Task 中

如果目标 Task 不存在,则新建目标 Task 和 Activity 实例,并将新建的 Activity 实例放入到新建的目标 Task 中

如果使用了 Intent.FLAG_ACTIVITY_CLEAR_TASK + Intent.FLAG_ACTIVITY_NEW_TASK 的组合,用户会看到很明显的切换 Task 的动画效果。

Intent.FLAG_ACTIVITY_CLEAR_TASK 的优先级很高,基本上可以无视所有的配置,包括其他的 LaunchMode 及 Intent flag。即使是设置了 singleInstance 的 Activity 所在的 Task 也会被清空,然后重建目标 Activity。

Intent.FLAG_ACTIVITY_SINGLE_TOP

Intent.FLAG_ACTIVITY_SINGLE_TOP 的作用是相当于把 Activity 的 LaunchMode 强制覆盖为 singleTop

添加了 Intent.FLAG_ACTIVITY_SINGLE_TOP 的 Activity,行为模式就变得和设置了 singleTop 的 Activity 一致,无论这个 Activity 在 AndroidManifest.xml 文件中设置的 android:launchMode 属性值是什么(因为 Intent flag 的优先级比 LaunchMode 高)。

Intent.FLAG_ACTIVITY_CLEAR_TOP

Intent.FLAG_ACTIVITY_CLEAR_TOP 的作用是清除在目标 Activity 实例之上的其他所有 Activity 实例(包括目标 Activity 自己的实例),然后重建一个目标 Activity 实例放入到目标 Task 中

Intent.FLAG_ACTIVITY_NEW_TASK + Intent.FLAG_ACTIVITY_SINGLE_TOP + Intent.FLAG_ACTIVITY_CLEAR_TOP 三个组合起来的作用是相当于把 Activity 的 LaunchMode 强制覆盖为 singleTask

添加了 Intent.FLAG_ACTIVITY_NEW_TASK + Intent.FLAG_ACTIVITY_SINGLE_TOP + Intent.FLAG_ACTIVITY_CLEAR_TOP 三个 flag 的 Activity,行为模式就变得和设置了 singleTask 的 Activity 一致。

应用场景

这里只讨论 LaunchMode 四大属性值的应用场景。因为添加 Intent flag 通常是为了动态修改 LaunchMode 的行为逻辑,这需要结合实际业务需求来具体分析。在这里讨论的是通用的场景。

LaunchMode应用场景
standard(默认)普通的 Activity
singleTop适合相同类型的 Activity。例如:接收 Notification 启动的内容显示页面、耗时操作的返回页面、登录页面等
singleTask适合能作为程序入口的 Activity。例如:WebView 页面、扫一扫页面、确认订单界面、付款界面等
singleInstance适合需要与程序分离开的 Activity。例如:闹铃的响铃界面、来电页面、锁屏页等

总结

本篇文章只是对于 Activity 启动模式中的一些概念进行了梳理,介绍了基本的 LaunchMode 四大属性值和几个常用的 Intent flag。

Activity 启动模式在实际的业务使用中会涉及 LaunchMode 和 Intent flag 各种各样的组合,其实际效果不可能靠着一篇文章就能说清楚的,只能靠大家在实际运用的时候进行尝试,才能明白其真正的效果。

参考

Android 面试黑洞——当我按下 Home 键再切回来,会发生什么?

聊聊 Activity 的启动模式

Android面试官装逼失败之:Activity的启动模式

Android 面试:说说 Android 的四种启动模式