Android 复习笔记 —— 任务栈和返回栈

7,284 阅读14分钟

距离上一篇博客,大概已经过去一个月了。

总结一下最近两周,大概就是睡一觉起来突然想换工作,然后被各路面试官吊打 ~

除了自身能力原因之外,准备不足的确也是很大的问题。所以我想把面试准备当做长期工作,把自己长期保持在一个 随时可以面试 的状态。

所以,这里是一个全新的专栏 —— Android 复习笔记 。记录我的 Android 复习之路,也希望可以帮助到你。

重学 Kotlin 一样,文章会在小专栏永久更新。传送门:

xiaozhuanlan.com/android

今天就来唠唠 任务栈返回栈

任务栈?返回栈?

关于 任务栈和返回栈,我看了 N 篇博客,说的最清楚的除了 重学安卓 ,那就非 官方文档 莫属了。其实大多时候,很多模糊不清的基本概念,从官方文档都可以轻松的得到你想要的答案。

官方文档中这一节的标题叫做 Understand Tasks and Back StackTask 就是我们常说的 任务栈Back Stack 就是返回栈

任务栈很好理解,Activity 们是存在一个栈结构中的,后进先出,这也很符合实际的使用场景。

依次打开 Activity1Activity2Activity3,它们依次入栈,如上图所示。然后连续按下两次返回键,Activity3Activity2 会依次出栈。

那么,返回栈呢?

什么是返回栈?

返回栈的作用是什么?

返回栈和任务栈的区别是什么?

灵魂三连拷问,不知道你能不能清晰的给出答案。这里暂且按住不表,我们先来看看 影响任务栈和返回栈的因素 有哪些?

我们从 launchMode(启动模式) 开始说起。

启动模式

声明启动模式有两种方式:

  1. 在清单文件中声明待启动的 Activity 的 launchMode 属性
  2. 代码中通过 Intent 启动 Activity 时,设置 flag

如果在一次启动过程中,两种方案都设置了,后者优先级比较高。

清单文件的 launchMode 和 intent flag 都不能完全代替对方。

launchMode 属性有四种取值 : standard 、 singleTop 、 singleTask 、 singleInstance  。

standard: 标准启动模式

也是默认的启动模式,每次启动 Activity 都会新建一个新的实例。待启动 Activity 会进入源 Activity 所属任务栈。

同一个 Activity 可能被实例化多次 。

singleTop: 栈顶复用模式

待启动 Activity 已经位于源 Activity 所属的任务栈的栈顶时,不会创建新的 Activity,而是直接使用栈顶的 Activity,并回调它的 onNewIntent 方法,onCreateonStart 不会被调用,直接回调 onResume

否则的话,在栈顶创建一个新的 Activity 实例。

singleTask:栈内复用模式

全局单实例,首先会寻找要启动的 Activity 想要的任务栈(默认或者 taskAffinity 属性指定),如果没有找到,则创建新的任务栈并将 Activity 实例放入。如果找到了想要的任务栈,这时候要判断栈中是否已经存在该 Activity 的实例,如果已经存在,会将该 Activity 以上的其他 Activity 实例弹出,把自己放到栈顶,同样也是回调 onNewIntentonResume。如果实例不存在,创建新的实例并压入栈中。

singleInstance:单实例模式

全局单实例,首次启动时会创建新的 Activity 实例,并放入一个新的任务栈中,且 这个任务栈中只会有这一个实例。 后续启动不会再新建实例。

默认的 standard 模式其实已经满足大部分情况下的需求,但是 同一个 Activity 会创建多次实例 在某些情况下肯定是不合适的,返回栈也会很突兀。这时候就需要复用已经存在的 Activity 实例,所以有了 singleTopsingleTask 两种不同的复用方式。而 singleInstance 则更加直接,Activity 实例和任务栈都是全局唯一的。

另外注意一点,singleTask 的 Activity 实例也是全局唯一的。可能有的人会问,在不同的任务栈中可能会存在重复的启动模式为 singleTask 的 Activity 实例吗?其实你仔细想一下就能发现,这是做不到的。

taskAffinity

前面提到了 Activity 想要的任务栈taskAffinity  的作用就是指定想要的任务栈。但它并不会在任何场景下都会起作用。

未显式声明 taskAffinity 的 Activity 都具有默认的任务栈,该任务栈的名称是应用包名。

当启动模式设置为 standard  或 singleTop  时,它是不起作用的。待启动的 Activity 会跟随源 Activity 的任务栈,即使你显式声明了不一样的 taskAffinity

当启动模式设置了 singleTask  或者 singleInstance  时,它就会新建任务栈来存储待启动的 Activity 实例。

除了 singleTask 和 singleInstance 以外,FLAG_ACTIVITY_NEW_TASK 也会使 taskAffinity 生效,后面会进行介绍。

返回栈的意义

在了解了上面的基础知识之后,我们可以来试着挖掘 返回栈的存在及其意义 。

官网上给了一个很好的例子来说明返回栈的存在,我就不搬官网的图了,画的并不是多么美观。我重新做了一张图。

图中虚线框表示任务栈,实线框表示返回栈。

Activity 1Activity 2 处于前台任务栈,即当前获得焦点的任务栈,它们的启动模式都是 standardActivity XActivity Y 处于后台任务栈,它们的启动模式都是 singleTask。在位于前台任务栈顶的 Activity 2 中启动处于后台任务栈的 Activity Y(跨应用启动) ,此时会把整个后台任务栈带到前台,并放到 返回栈 的栈顶。此时,X 和 Y 的 taskId 是一致的,1 和 2 的 taskId 是一致的,它们仍然处于各自的任务栈中,但返回栈中自顶而下依次是,Y -> X -> 2 -> 1 。此时按下返回键,并不会回到 Activity 2,而是先回到 Activity X 。

从上图中可以清晰的看到 **任务栈和返回栈是独立存在的,用户页面的返回依赖的是返回栈,而不是任务栈。一个返回栈中可能会包含来自不同任务栈的 Activity ,以维护正确的回退栈关系。**这就是返回栈存在的意义。

如果 Activity X 和 Y 的启动模式都是 standard 呢 ?会直接在 Activity 2 所属的任务栈顶直接新建一个 Y 实例 ,Activity 2 的返回栈中依次是 Y -> 2 -> 1 。此时,两个应用的返回栈各不干扰。下图展示了 X 和 Y 都是 standard 的情况。

同样,singleTop 也不行,和 standard 表现一致。

Intent Flag

影响启动模式,任务栈和返回栈的另一种方式就是为 Intent 设置启动标记。

设置启动标记的方法有如下两个:

public @NonNull Intent setFlags(@Flags int flags) {
        mFlags = flags;
        return this;
}

public @NonNull Intent addFlags(@Flags int flags) {
        mFlags |= flags;
        return this;
}

一个是 设置,一个是 添加,在使用的时候要注意。

Intent flag 有很多,这里挑选比较经典的三个 flag , NEW_TASKCLEAR_TOP 、 SINGLE_TOP  。

FLAG_ACTIVITY_NEW_TASK

首先,在不设置 taskAffinity 的情况下,单独设置 FLAG_ACTIVITY_NEW_TASK 并没有任何意义,不会创建新的任务栈,每次启动都会创建新的 Activity 实例,不会 栈内复用

对了,为什么要提到 栈内复用 呢?那不是 singleTask 的特性吗?

网上很多关于 Activity 启动模式的文章,都会这么说:

官方文档上说,FLAG_ACTIVITY_NEW_TASK 和 singleTask 的行为一致。其实这是不正确的。

正如我前面所说的,单看这句话,它们的行为的确不一致。那么,官方文档真的在传递错误的认知吗?

先来看看这些网文的论据,也就是官方文档上的原话:

Start the activity in a new task. If a task is already running for the activity you are now starting, that task is brought to the foreground with its last state restored and the activity receives the new intent in onNewIntent(). This produces the same behavior as the "singleTask" launchMode value, discussed in the previous section.

细品,它表达的其实是,在一个新的任务栈中启动 Activity 。如果想要的任务栈已经存在,并且其中已经运行着待启动的 Activity ,那么这个任务栈会被带到前台,并回调 onNewIntent() 。这个行为和 singleTask 一致。

还拿 返回栈的意义 一节中的例子做实验, Activity X 和 Y 的启动模式都设置为 standard,搭配 FLAT_ACTIVITY_NEW_TASK 启动,不设置 taskAffinity ,其实也能达到和 singleTask 基本一样的返回栈效果。

但并不是完全相同,这样产生的返回栈是 Y -> Y -> X -> 2 -> 1 。对照下面的任务栈和返回栈捋一捋。

会有两个 Y 实例?standard 嘛,没毛病。换成 singleTask 就好了,只有一个实例。等等,换成 singleTask,那不又变成上面的例子了,也就不需要设置 FLAG_ACTIVITY_NEW_TASK 了,禁止套娃!

我可以用 singleTop 嘛,这样就真的和前面的 singleTask 中提到的例子完全表现一致了。

这也间接说明了,如果已经设置了 launchMode 为 singleInstance 或 singleTask,是没有必要添加 FLAG_ACTIVITY_NEW_TASK的 。从源码也有所体现。

startActivity 过程中关于 flag 的计算在 ActivityStarter.java  类中的 startActivityUnchecked()  方法中的 computeLaunchingTaskFlags()中 :

private void computeLaunchingTaskFlags(){
    ......
    if (mInTask == null) {
        if (mSourceRecord == null) {
            // 1. 由非 Activity 环境启动
            if ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 && mInTask == null) {
                mLaunchFlags | = FLAG_ACTIVITY_NEW_TASK;
            }
        } else if (mSourceRecord.launchMode == LAUNCH_SINGLE_INSTANCE) {
            // 2. 源 Activity 的启动模式是 SingleInstance
            mLaunchFlags | = FLAG_ACTIVITY_NEW_TASK;
        } else if (isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)) {
            // 3. 待启动 Activity 的启动模式是 singleInstance 或者 singleTask
            mLaunchFlags | = FLAG_ACTIVITY_NEW_TASK;
        }
    }
}

从上面的注释 3 处可以看到,当启动模式是 singleInstance 或者 singleTask 时,系统会自动添加FLAG_ACTIVITY_NEW_TASK 标记。

FLAG_ACTIVITY_NEW_TASK  更被大家所熟知的用法可能是 从非 Activity 环境启动 Activity 。

默认情况下,待启动的 Activity 会进入源 Activity 所在的任务栈中。如果是从 非 Activity 环境启动,例如 Service,Broadcast,Application 等,根本不存在与之对应的任务栈,AMS 无从推断该把 Activity 放入哪个任务栈,就会抛出一个著名的异常 Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag.  。

这个异常是在 ContextImpl.startActivity() 方法中抛出的:

    @Override
    public void startActivity(Intent intent, Bundle options) {
        warnIfCallingFromSystemProcess();

        final int targetSdkVersion = getApplicationInfo().targetSdkVersion;

        if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
                && (targetSdkVersion < Build.VERSION_CODES.N
                        || targetSdkVersion >= Build.VERSION_CODES.P)
                && (options == null
                        || ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
            throw new AndroidRuntimeException(
                    "Calling startActivity() from outside of an Activity "
                            + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                            + " Is this really what you want?");
        }
        mMainThread.getInstrumentation().execStartActivity(
                getOuterContext(), mMainThread.getApplicationThread(), null,
                (Activity) null, intent, -1, options);
    }

** 首先会检测是否设置了 FLAG_ACTIVITY_NEW_TASK ,如果设置了,才会调用 Instrumentation.execStartActivity() 。

FLAG_ACTIVITY_NEW_TASK  会告知待启动的 Activity 放进一个新的任务栈中。其实到底是不是 “新” 的任务栈,这是还是由 taskAffinity 来决定的,这个在前面也讨论过了。所以,没有显示声明 taskAffinity 的 Activity ,在 非 Activity 环境中 中仅仅通过 FLAG_ACTIVITY_NEW_TASK 启动的话,还是会进入默认的任务栈中。

FLAG_ACTIVITY_CLEAR_TOP

CLEAR_TOP 在单独使用时,如果想要的任务栈中已经存在待启动的 Activity 的实例,则会将该 Activity 实例之上的其他 Activity 弹出,把自己放到栈顶,并回调 onNewIntent 。但这是有前提的,就是待启动的 Activity 的 launchMode 不能是 standard 。

如果是 standard ,则会把自己及之上的所有 Activity 全部弹出,新建一个实例放入。

FLAG_ACTIVITY_SINGLE_TOP

等同于 singleTop ,栈顶复用。即使待启动的 Activity 是 standard ,如果已经处于栈顶的话,也会复用。

接下来介绍一些在清单文件中使用的,可以控制任务栈和返回栈 Activity 属性。

Activity 属性

allowTaskReparenting

允许转移任务栈 。根据官方文档以及各路网文介绍,它的作用应该是这样的:

从 App1 的页面 A 跳转到 App2 的页面 B,页面 B 设置了 allowTaskReparenting=true  。此时,由于是页面 A 启动了页面 B,所以 页面 B 是处于 App1 的任务栈中。然后点击 Home 键回到桌面,再点击 App2 的桌面图标,此时启动的应该是 页面 B 。相当于页面 B 从 App1 的任务栈中转移到了 App2 的任务栈中。

但是,事实情况是,我没有复现出这样的场景。我的测试环境是这样的:由于页面 A 和页面 B来自两个不同的 App ,所以我没有特地设置 taskAffinity ,因为本来就不一样。页面 B 的启动模式为 standard 。

操作步骤如下:

App1 的页面 A -》 App2 的页面 B -》 Home 键 -》 App2 launcher

这样测试弹出来的并不是页面 B,而是 App2 的首页。不知道我的测试步骤有没有什么纰漏。大家也可以测试一下。

把 页面 B 的启动模式 改为 singleTask ,可以产生类似的效果。但是页面 B 并不会进入到 页面 A 的任务栈,这是 singleTask + taskAffinity 的效果,其实和 allowTaskReparenting 是没有关系的。

不知道大家怎么理解这个属性,可以在评论区和我交流一下。

=== 分割线

2020 年 8 月 4 日更新:

终于摸索到了 allowTaskReparenting 的正确用法 —— 任务栈转移大法

以我的 demo 中的示例代码为例,AllowTaskReparentingActivity 是 App1 中的 Activity,包名即默认任务栈是 luyao.android ,但我们给它设置一个不一样的 taskAffinity—— luyao.android2 , 即 App2 的默认任务栈,并设置 allowTaskReparenting="true",如下所示:

<activity android:name=".activity.AllowTaskReparentingActivity"
           android:label="AllowTaskReparenting"
           android:taskAffinity="luyao.android2"
           android:allowTaskReparenting="true"/>

操作流程如下 Gif 所示:

在 App1 中经历 StandActivityA -> AllowTaskReparentingActivity ,可以看到它们的 taskId 是一致的,说明它们处于同一个任务栈中。为什么设置了 taskAffinity ,还在同一个任务栈中呢?因为默认的启动模式是 standardtaskAffinity 并不会起作用。但是好像也并不是完全没有作用,接着看下面的操作。

然后按下 Home 键,返回桌面。再启动 App2 。正常情况下应该启动 App2 的 MainActivity,但是由于 allowTaskReparenting 的作用,这里启动的是 AllowTaskReparentingActivity,此时按下返回键,就回到了 App2 的 MainActivity

细心的读者可能也看到了,standard 模式下,App2 中的 AllowTaskReparentingActivityMainActivity 也并非在用一个任务栈,并没有发生所谓的 任务栈转移 ,只是对回退栈做了一些处理。感兴趣的读者可以 clone 下来代码尝试一下其他启动模式下的表现。

那么,allowTaskReparenting 有什么具体的应用场景呢?这个我也不清楚。上面的示例代码在我手里的 MIUI 和 Android 虚拟机下的原生 ROM 中表现根本不一致,更不用说各大手机厂商的魔改系统了。

clearTaskOnLaunch

如果任务栈的根 Activity 被设置了 clearTaskOnLaunch=true ,那么当按下 Home 键返回桌面,再重新点击桌面图标进入应用时(从最近任务列表进入不会有效果),根 Activity 以上的其他 Activity 全部弹出,只留下自己。

如果设置 clearTaskOnLaunch=true 的 Activity 不在任务栈底,是没有效果的。

alwaysRetainTaskState

clearTaskOnLaunch 相反,它要做的是尽量保持任务栈中的所有实例不被销毁。在不设置 alwaysRetainTaskState 的默认情况下,可能由于各种原因任务栈会被清理,仅仅留下根 Activity 。

它也只有设置在 根 Activity 才会有效,设置给其他 Activity 是无用的。

finishOnTaskLaunch

和 clearTaskOnLaunch 效果一致,但它只对设置 finishOnTaskLaunch=true 的当前 Activity 有效。即按下 Home 键返回桌面,再点击桌面图标重新进入应用时,任务栈中 finishOnTaskLaunch=true 的 Activity 会被移出任务栈。

它的所有 Activity 有效,包括根 Activity 。

excludeFromRecents

当前 Activity 所在任务栈是否在最近任务列表中显示。只有设置在根 Activity 上才有效果。

autoRemoveFromRecents

先来问一个问题,进入 App ,从 A 跳到 B ,从 B 跳到 C ,再按返回键直到回到桌面。这时候查看最近任务列表,里面可以看到这个 App 吗?答案是可以的。

而  autoRemoveFromRecents 的作用就是当任务栈中的所有 Activity 都被移除时,自动不在最近任务列表中显示。

最后

启动模式taskAffinityIntent flag ,这三个属性排列组合起来会产生各种各样的任务栈和返回栈效果。

不要相信任何网文的结论,包括我上面所说的。 自己动手敲一敲,才能从容的面对面试官的各种 “排列组合”。如果你实在不想敲,我这里有现成的实例代码,包含了我在写这篇文章的过程中所有的验证代码。

配套代码传送门


沉寂一个多月,我又回来输出了。关注我,不迷路!