文章目录
  1. 1. launchMode属性
  2. 2. taskAffinity
  3. 3. intent flags
  4. 4. 任务栈和回退栈
  5. 5. 参考文章

我们知道, Activity需要配置在AndroidManifest.xml中才能正常启动, 其原因是launcher需要它来判断应当从哪个Apk中启动哪个Class. 在配置Activity的时候, 有一个属性叫做launchMode, 它对Activity的启动和退出影响颇大, 今天我们就来深入了解一下这个属性以及其背后的逻辑.

launchMode属性

本节内容可自行写demo验证理解

翻阅各种参考, 我们知道(截至本文成文之日)launchMode一共可以指定四种属性值, 分别是standard singleTop singleTask singleInstance, 其中standard也是默认的属性值. 那么他们分别具有怎样的作用呢?
总的来说, launchMode属性影响的是Activity的回退栈, 因此我们有必要先理解回退栈这一概念.
Activity启动的时候, 我们需要考虑当它退出的时候, 下一个要展示的Activity是什么, 为此, android中维护一个Task, 称为任务栈, 以栈的数据结构存储启动的Activity. 当我们退出一个Activity的时候, 展示上一个启动的Activity, 于是在Task中, 只要让当前展示的Activity位于栈顶, 在退出时让它出栈, 就可以自然完成这样的回退逻辑. 例如从A启动B, 当我们退出B的时候, 就展示A. 以上就是最简单朴素的回退栈. 但是这里有一个问题, 就是当我们从桌面启动A时, 回退栈中似乎只有一个A, 这时候退出A, 栈就空了, 怎么办呢? 当然我们大家都知道, 这时候其实是回到了桌面的. 原因就在于, Task有两层概念, 第一层是, 任一应用(作为一个process)拥有自身的回退栈, 第二层是, 系统维护跨应用的全部回退栈. 于是在退出A的时候, 我们实际上是退出了当前应用的回退栈, 交由系统决定下一个要展示的回退栈, 这里就是launcher这个系统应用的回退栈了. 而在launcher中, 由于重写了Back Home按键的响应, 所以不会再退出这个应用.
可以用图表示如下:

回退栈示意图

有了回退栈我们就知道了如何处理”退出一个Activity”这件事情. 与此相对的, “启动一个Activity”这件事情就意味着需要将Activity放到某个回退栈中. 如果我们完全按照以上的基本逻辑, 即每个应用拥有自身的回退栈, 并且每启动一个Activity就将它放到栈顶, 这就是launchMode的默认属性standard的行为. 对于standardActivity来说, 它的实例会被放到启动它的Activity所在的Task中.

很可惜现实世界不是这么简单而美好的, “标准行为”总是会引起”标准错误”. 于是就有了后面三种启动模式.
考虑一种情况, 我们需要在应用App1中启动应用App2的某个Activity B. 假设这个B以标准模式启动会有什么问题呢? 每次调用都会创建B并添加到任务栈中, 导致任务栈上茫茫多的B, 退都退不完. 更糟糕的是, 默认情况下B没有指定任务栈, 它会被放在调用它的应用的任务栈上, 这就导致你回到原来的应用界面需要穿过千山万水. 这种情形很常见, 例如点击通知栏通知, 就会跳转到指定应用的指定Activity, 点一百次通知就创建一百个实例这是不可接受的. 通常情况下其实只需要更新数据, 根本没必要新建一个实例. 于是我们有了singleTop模式. 它的作用在于, 该模式下的Activity实例, 如果本身已经在栈顶, 那么再次调用startActivity的时候, 不会创建新的实例, 而是调用已存在实例的onNewIntent()生命周期, 处理新传入的Intent. 如果实例不存在或不在栈顶, 那就跟标准模式一样创建新的实例并放在栈顶.
考虑另一种情况, 我们需要在A-B两个Activity中反复互相启动(秘技:反复横跳!), 那么就会引起问题. 由于标准模式下只是无脑启动并添加Activity, 就会导致回退栈中存有大量的A-B-A-B-A-B这样的序列, 而实际上存在多个A还是一个A对用户来说没有分别, 反而多个A导致退出的时候需要按很多次Back, 不胜其烦, 而singleTop模式也不能解决这种问题. 于是我们有了singleTask模式. 它的作用在于, 处于singleTask模式的Activity, 在整个Task之中只能有一个实例. 在启动该Activity的时候, 如果Task中没有该实例, 则新建实例并添加到任务栈, 走全新创建的生命周期, 否则将任务栈中处于该实例以上的所有Activity出栈, 令它处于栈顶(唯我独尊有木有!), 然后调用onNewIntent()周期. 它也是可以快速清空栈内Activity的黑科技.(思考: singleTask模式一定会新建一个Task吗?)
再考虑另一种情况, 某个Activity的功能非常单一, 我们全局只需要一个这样的实例来完成他的功能就可以了, 相当于要一个单例Activity. 这样的话, 以上三种模式都无法满足需要, 他们都是强调在某一个Task栈中的状态, 但处于不同的Task栈的话就没有限制了(特别说明一下singleTask模式, 它令Task中只有一个实例, 按理说如果我们只有一个Task的话也可以满足单例的要求, 但它的”副作用”是会清空实例以上的全部Activity, 这却不是我们希望的效果), 所以我们需要一个新的模式来代表这种跨Task栈的单例, 由此诞生了singleInstance模式. 处于该模式的Activity, 在被启动的时候会新建一个Task, 整个生命周期都在这个Task中, 并且这个Task有且只有它一个实例, 无论从哪里启动它, 只要在某个Task中存在实例, 就会调用这个实例的onNewIntent()周期, 而不是新建一个实例. 这样的例子还是挺多的, 例如桌面日历, 桌面时钟.(思考: 如果指定另一个Activity跟这个singleInstanceActivity拥有同样的Task, 会怎样? 如果从这个singleInstance实例中启动新的Activity, 新的Activity会放在哪里?)

以上就是四种启动模式的特点. 在官网中对启动模式的描述, 隐含条件是非标准启动模式都指定了taskAffinity属性, 故可能会引起误解.

taskAffinity

启动模式可以配合taskAffinity属性使用.这里来解释一下taskAffinity这个属性.
它是ApplicationActivity标签都有的属性, 用于指定一个Task的名字, 直译的话可以译作”任务归属性”或者”任务亲和性”吧. 不填写该属性的情况下, 所有Activity在一个以包名为名字的Task中, Activity指定了该属性时, 会被放在以该属性值为名字的Task中, Application指定了该属性时, 整个Application的默认Task以该属性值为名.
前文曾提出疑问, singleTask是否一定会新建一个Task, 答案是”不一定”. 以singleTask模式启动的Activity, 总是在”将要放置该Activity的Task”中检查实例, 而不指定taskAffinity的情况下将要放置该Activity的就是默认Task了. 实际上只有在指定了taskAffinity并且该Task还不存在的情况下, 才会新建以taskAffinity值命名的Task.
需要注意的是, taskAffinity属性是跨应用的, 你完全可以为不同的应用指定同一个Task. 要使该属性生效, 必须允许在启动拥有该属性的Activity时新建Task, 即需要传入下文将要介绍的FLAG_ACTIVITY_NEW_TASK标志或为Activity指定allowTaskReparentingTrue.

intent flags

如果说, 我们想要在代码中动态地改变Activity启动模式, 比如想让本来以standard模式启动的Activity在某种情况下以singleTop模式启动, 这可以办到吗? Android工程师们显然想到了这种情形, 设计了intent flags来达成该目的.
在启动Activity的时候需要一个Intent, 通过Intent.setFlags()可以设置启动标志, 其中启动模式和FLAG的对应关系如下:

启动模式 启动标志
standard 不设置
singleTop FLAG_ACTIVITY_SINGLE_TOP
singleTask FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP

另外还有更多的FLAG可以达成其他效果, 查看Intent类源码可以了解更多.
细心的同学可能发现了, 上述表格跟官网的介绍有出入. 官网说仅FLAG_ACTIVITY_NEW_TASK就会产生与singleTask模式相同的行为, 事实上经我实验只有在指定了taskAffinity时这句话才成立. 官网说FLAG_ACTIVITY_CLEAR_TOP属性没有对应的启动模式, 事实上确实如此, 因为它是singleTask模式的其中一个行为.

细心的同学可能还发现, 表格中没有singleInstance模式对应的FLAG. 那是因为这种模式要达成的效果需要在处理Intent之前更早的时期来控制, 在Intent中设置没用.

任务栈和回退栈

由前文所述我们接触到两个概念: 任务栈和回退栈. 我姑且在此拙劣地理解一下: 任务栈是为了完成某种任务而形成的Activity的栈; 回退栈是显示回退顺序的Activity栈. 如此理解的话, 那么两者是从不同的侧面来描述Activity的组织方式, 其中任务栈就是我们上文一直在说的Task, 而回退栈只是一个逻辑上的概念. 事实上我们在新建或者唤起一个Activity的时候, 它所在的整个Task会被提到最上层, 也就是”回退栈”的顶端, 就算launcher应用也是以这种方式回到栈顶的, 只不过它对BackHome做了处理, 我们无法通过返回键令它出栈而回到位于它下面的Activity. 这就是为什么我们能够在不同的应用间切换, 还能保持应用所打开的Activity的原因(事实上通过设置clearTaskOnLaunch等属性, 切入后台后再切回来可以清空已打开的Activity). 一个栈的名字由在这个栈的底端的那个Activity来定义, 默认就是那个Activity所在的应用的包名.
最后一个思考题, 这些任务栈是被谁在什么时候进行管理的呢?
祭出我们的大杀器: 源码. 全目录搜索关键词launchMode, 然后就会发现一个东西./frameworks/base/services/java/com/android/server/am/ActivityStack.java以及另一个东西./frameworks/base/core/java/android/content/pm/ActivityInfo.java, 其中前者我们在AMS中已经见到过了, 至此得解.

参考文章

文章目录
  1. 1. launchMode属性
  2. 2. taskAffinity
  3. 3. intent flags
  4. 4. 任务栈和回退栈
  5. 5. 参考文章