面试官:任务栈?返回栈?启动模式?傻傻分不清楚?年轻人耗子尾汁

最后

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

最后针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

我们从 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 的任务栈中。

最后

考虑到文章的篇幅问题,我把这些问题和答案以及我多年面试所遇到的问题和一些面试资料做成了PDF文档

喜欢的朋友可以关注、转发、点赞 感谢!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值