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

对了,为什么要提到 栈内复用 呢?那不是 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 是没有关系的。

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

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

以我的 demo 中的示例代码为例,AllowTaskReparentingActivity 是 App1 中的 Activity,包名即默认任务栈是 luyao.android ,但我们给它设置一个不一样的 taskAffinity—— luyao.android2 , 即 App2 的默认任务栈,并设置 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 ,这三个属性排列组合起来会产生各种各样的任务栈和返回栈效果。

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

配套代码传送门

面试复习笔记:

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
给文章留个小赞,就可以免费领取啦~

戳我领取:Android对线暴打面试指南超硬核Android面试知识笔记3000页Android开发者架构师核心知识笔记

《960页Android开发笔记》

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

总结

现在新技术层出不穷,如果每次出新的技术,我们都深入的研究的话,很容易分散精力。新的技术可能很久之后我们才会在工作中用得上,当学的新技术无法学以致用,很容易被我们遗忘,到最后真的需要使用的时候,又要从头来过(虽然上手会更快)。

我觉得身为技术人,针对新技术应该是持拥抱态度的,入了这一行你就应该知道这是一个活到老学到老的行业,所以面对新技术,不要抵触,拥抱变化就好了。

Flutter 明显是一种全新的技术,而对于这个新技术在发布之初,花一个月的时间学习它,成本确实过高。但是周末花一天时间体验一下它的开发流程,了解一下它的优缺点、能干什么或者不能干什么。这个时间,并不是我们不能接受的。

如果有时间,其实通读一遍 Flutter 的文档,是最全面的一次对 Flutter 的了解过程。但是如果我们只有 8 小时的时间,我希望能关注一些最值得关注的点。

(跨平台开发(Flutter)、java基础与原理,自定义view、NDK、架构设计、性能优化、完整商业项目开发等)

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

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

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

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值