重学安卓1-4:拎清 Activity 任务和返回栈

作品简介
往期回顾专栏目录更新动态优惠政策版权须知
温馨提示:如这是第一次接触《重学安卓》,可通过上述链接快速了解《重学安卓》专栏,获取 专栏目录、试读内容、更新动态 和 发展状况。
截至目前,专栏已对 体系化文章 实施 3310 余次修订,数十位群友告诉我,受专栏启发 亦开启了写作之路。群里不定期会有小伙伴讨论适配问题、分享原创开源库 和 提供内推机会,订阅后可随时进群交流。

·

Note 2021.5.25 重要提示
阅读本文最佳时机是,您已吃过 阅读 “源码” 或 “源码分析文” 时 找不到头绪的苦
您还没吃过苦,那您先不要着急阅读本文。您得吃过苦,才会有体会。
在您吃够这方面苦后,您才有机会发现,本文正是专用于解决 “如何找到正确打开方式” 的困扰。
我们绝不通篇贴源码,而是基于广泛实践和反思,在累积过大量样本 乃至足以排除掉所有干扰信息后,点到为止揭露 Activity 任务栈 & 返回栈 最核心本质,方便您理解其真实的存在意义,乃至笃信使用项目中。

前言

关于任务和返回栈,相信大家都见过官网这张配图:

不幸的是,官网《任务和返回栈》篇 关于 “任务和返回栈” 的描述若有似无、含糊其辞,网上搜集到的资料也多是照抄官网,未经检验和修补逻辑漏洞,

例如关于上图,一眼便能注意到和理解的信息有哪些?任务和返回栈到底是什么关系?是同一个东西,还是两个独立的存在?又或者是包含关系?二者关系在上图是否有所体现?
步骤一 Back Stack 和下方的 Background Task,一个是 Back Stack(假设这就是所谓 “返回栈”),一个是 Task(假设这就是所谓 “任务”),那为何到了步骤二,这 Task 中的 Activity 又能跑到 Back Stack 中?所以它俩到底什么关系?

正因 “前置知识” 的残缺不全、关键信息处于 “盲区”,乃至涉及 “窗口页面切换” 的场景需求,开发者往往举步维艰,难以匹配到符合预期的方案,

故这期我们继续 “追根溯源”,补足关于 “任务与返回栈” 的来龙去脉,并在文末设计一个小实验,来拆解上述配图的含义,相信阅读后你会豁然开朗。

文章目录一览

  • 前言
  • 1.任务和返回栈
  • 1.1.任务和返回栈,概念分别为何?
  • 1.2.为何分别存在任务和返回栈?
  • 2.启动模式
  • 2.1.为何存在 “启动模式” 的设计?
  • 2.2.四种启动模式,表面上的特点分别为何?
  • 2.3.为何存在多种 “启动模式” 的设计?
  • 2.4.如何设置启动模式?
  • 2.5.SingleTask 如何指定与哪个任务关联?
  • Note 2021.4.30 加餐:
  • DeepLink 和 allowTaskReparenting 实验的复现方式 和 注意事项
  • 3.任务清空或保留 Activity 的几种方式?
  • Note 2020.11.18 加餐
  • 对启动模式和 FLAG 区别的补充说明
  • 4.综合案例(证实 “返回栈” 相对于 “任务” 的独立存在,全程动图记录)
  • Note 2020.6.17 加餐:
  • 综合实验简化重制版
  • 5.福利时间到:魔鬼就在细节中 —— 全网独家严密测试的 5 个结论:
  • taskAffinity 真实的适用范围
  • standard 和 singleTop 的真实本质
  • singleTop 的真实本质
  • singleTask 的真实本质及实验佐证
  • singleTask 和 singleInstance 与最近访问列表的关系
  • Note 2020.5.17 加餐
  • 一个 app 能有多少个 ActivityStack
  • Note 2020.07.27 加餐:
  • 如何在打印信息中正确区分 TaskRecord 和 ActivityStack
  • Note 2020.08.10 加餐:
  • 通过 Android 9.0 的新 ADB 命令获取简洁的 Activity 堆栈信息
  • 最近访问列表中展示的卡片到底是什么
  • Note 2020.5.28 加餐:
  • 对 5.17 疑问的实验检验结果
  • Note 2020.6.29 加餐:
  • SingleTask 和 SingleInstance 不新建 Task 的特殊情况
  • 为什么 SingleInstance Activity 回退直接回到桌面
  • Note 2021.6.30 加餐:
  • 系统为何不设计为直接通过 ActivityStack 来管理 ActivityRecord?

1.任务和返回栈

1.1.任务和返回栈,概念分别为何?

ActivityRecord

描述 Activity 的相关信息,对应着一个用户界面,是 Activity 管理的最小单位。

TaskRecord

是一个栈结构,管理着多个 ActivityRecord,栈顶的 ActivityRecord 表示当前获得焦点的界面。

ActivityStack

是一个栈结构,管理着多个 TaskRecord,栈顶的 TaskRecord 表示当前获得焦点的任务。

Note 2023.4.16: ActivityStack 使用的 “栈” 结构并非传统意义上的 “后进先出” 栈,而是根据 “窗口焦点切换” 场景需求定制的 “栈”,也即该 “栈” 不仅能根据返回键指令将栈顶的元素出栈,还能根据 “窗口焦点切换” 等情景将栈中元素的位置对调,例如将栈底元素置顶等,
也因此,该 “栈” 常以 ArrayList 方式实现,对此可参见 Android 10 源码 ActivityStack 的 ArrayList< TaskRecord >。
Note 2020.1.25:对 “焦点、前景、可见” 等概念不熟悉的朋友,建议先阅读一下这篇短文 《重学安卓:Activity 生命周期 3 个辟谣》,文中通过介绍 RemixOS 等 Android 桌面操作系统,方便无障碍理解 “前景和可见” 的区别,以及何谓 “获得焦点”。

ActivityStackSupervisior

管理着多个 ActivityStack。当前只会有一个获得了焦点的 ActivityStack

划重点 👆 👆 👆 这一段全是重点。

1.2.为何分别存在任务和返回栈?

通常说的任务和返回栈,分别指的是 TaskRecord 和 ActivityStack,它们的存在 主要是为了维护 “页面跳转的连贯性” 体验

划重点 👆 👆 👆

比如用户在日记软件中添加照片,于是点击添加按钮,跳转到系统相册的选择器模式,选取照片后,又跳回日记软件。虽然日记软件和系统相册的页面来自两个 App,但这一系列的操作给用户的感觉,就好像是在同一个 App 中完成。

换言之,即使是来自不同 App 的 Activity,也能存在于同一个任务中,完成连贯的跳转和回退操作。至于为何有了任务还要有返回栈,我们暂且按住不表、先往下阅读~

2.启动模式

2.1.为何存在 “启动模式” 的设计?

启动模式是用于定义 Activity 与任务的关联方式

划重点 👆 👆 👆

为适应不同场景的需要,总共设计了 4 种方式:

standardsingleTopsingleTasksingleInstance

2.2.四种启动模式,表面上的特点分别为何?

standard - 标准模式

创建 Activity 的实例,并 添加到启动它的源 Activity 所在的任务的栈顶。不管栈内或栈顶是否已存在该 Activity 的实例。

singleTop - 栈顶复用模式

启动它的源 Activity 所在的任务的栈顶已存在 该 Activity 的实例,那么不创建该 Activity 的新实例 —— 而是走该 Activity 实例的 onNewIntent 回调,注入新的 intent,并执行 onResume(也就是不走 onCreate、onStart)。

否则就在栈顶创建一个新实例。

singleTask - 栈内复用模式

该 Activity 所属的任务中已存在 该 Activity 的实例,那么不创建该 Activity 的新实例 —— 而是 首先将任务中该 Activity 实例之上的 Activity 全都出栈,并且走该 Activity 实例的 onNewIntent 回调,注入新的 intent,并执行 onResume。

否则就在栈顶创建一个新实例。

(singleTask Activity 的所属任务,取决于清单中配置的 taskAffinity,如果没有用 taskAffinity 指定任务名,默认是 Activity 所属 App 的默认任务。下文会介绍 taskAffinity 的作用。)

singleInstance - 单例模式

会新建一个任务,并且 独享这个任务。也即 整个系统 有且只有 这么一个 Activity 的实例,多个 App 可共享该实例。

Note 2023.4.9 加餐
singleInstancePerTask 模式:
根据小伙伴 smartlyb 在评论区 118 楼的反馈,Android 12 新增 singleInstancePerTask 启动模式,笔者认为该模式的存在,主要是为了更好的支持 “平板、桌面” 等场景下,窗口多开的需求,
例如对于 “文件从目录 A 复制到目录 B” 的场景,用户通常会在文件管理器中 “在新窗口中打开当前目录”,也即打开两个相同的窗口界面(比如 CategoryActivity),
根据下文的分析,每个窗口由 task 来对应,因而从 android 平台的实现角度来说,需要开两个 task,并且每个 task 都承载 CategoryActivity 的不同实例,
换言之,singleInstancePerTask 的存在是为了支持 Activity 多开不同实例、独占不同的 task、且相互之间不发生干扰
也即 singleInstancePerTask 和 singleInstance 的区别在于,一个 Activity 类可以存在多个实例,每个实例都独占一个 task

2.3.为何存在多种 “启动模式” 的设计?

standard,顾名思义,是默认的启动方式。

但为每个 Activity 都创建一个新的实例,开销太大了,因而有了 singleTop 和 singleTask 这两种复用模式,例如分别用于 “Activity 在栈顶时基于新 Intent 完成纯刷新视图状态”,以及 “想要快速出栈多个 Activity 并返回到某个底层页面” 的场景。

划重点 👆 👆 👆

又考虑到多个 App 可能需要共享同一个实例,因而又设计出 singleInstance 的复用模式。

上文我们提到了 4 种模式各自的特点,但模式之间究竟有何本质区别,这个我们在下文中会重点提到。

2.4.如何设置启动模式?

设置的方式通常有两种:

静态配置方式:在清单文件中,为 Activity 指定启动模式,例如:

动态代码方式:在代码中,通过 Intent setFlag,为 Activity 指定启动模式,例如:

常用 FLAG 有以下几种:

注:为了更好的显示 FLAG 本身作用,以下对 FLAG 的谈论,都是以 “启动模式为 standard 的 Activity” 为例来赋予 FLAG,并与启动模式为 SingleTop 或 SingleTask 等的 Activity 在启动时的效果做对照说明。

FLAG_ACTIVITY_NEW_TASK,近似于 singleTask 模式。当在清单中为 Activity 设置 taskAffinity 属性时,能跳转到指定任务(若先前不存在该任务,则先创建该任务)。该 FLAG 通常用于从非 Activity 的环境下启动 Activity(这么设计,是为了给 Activity 一个容身之处)。

FLAG_ACTIVITY_SINGLE_TOP ,对应着 singleTop 模式。

FLAG_ACTIVITY_CLEAR_TOP,近似于 singleTask 模式。当该 Activity 已存在于任务中,该 Activity 之上的 Activity 都会出栈,并且该 Activity 如为 standard,则会被重新创建,如为 singleTop,则是走 onNewIntent。

所以 即使 FLAG_ACTIVITY_NEW_TASK 与 FLAG_ACTIVITY_CLEAR_TOP 共同使用,其效果也并不完全等同于 singleTask 模式

Note 2020.11.18 加餐:
感谢读者 @Jackie 的反馈和补充,此处的意思即,启动模式和 FLAG 的区别主要在于,前者是固定的,后者是可组合的,例如 singleTask 是固定的、而 ClearTop 是可组合的:当你多数场景下不合适使用 SingleTask,但个别场景又需要它的这种清空栈中上方 Activity 的特性,那么此时你就选用 singleTop + clearTop FLAG 的组合,如此,在清单中你配置的是 singleTop,仅在你需要清空效果的场景下,在代码中动态附加 FLAG 来组合使用。
划重点 👆 👆 👆

FLAG_ACTIVITY_NO_HISTORY,被指定的 Activity 在跳转到其他 Activity 后,将从任务中移除。

FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,被指定的 Activity 不出现在 “最近应用” 列表中。

2.5.如何指定与哪个任务关联?

launchMode 属性可以指定以何种模式去与任务关联,那么如何指定具体与哪个任务关联呢?

可以通过在清单文件中为该 Activity 设置 taskAffinity 属性。

默认情况下 App 的任务名称为包名,因而我们这里为 taskAffinity 设置一个不一样的属性值,例如 “com.kunminx.task1”,来指定与该任务关联。如果返回栈中不存在该任务,则会新建一个该任务。具体是在哪个返回栈新建,取决于启动该 Activity 的 Activity 所在任务所在的返回栈

划重点 👆 👆 👆


创作时间: