Android launchMode

该篇文章中的所有图文描述均来自扔物线视频 Android 面试黑洞——当我按下 Home 键再切回来,会发生什么?。文章仅用于方便记忆总结复习使用。

很多 Android 工程师在投简历找工作之前,会去复习一下 Activity 的 launchMode 启动模式,因为面试的时候会考,但真正把它搞懂的人是很少的,包括很多拿它做面试题的面试官。

当用户在使用 App 的时候,按下了 Home 键,然后再切回来,或者在多个 App 之间切来切去,App 的内容会不会改变,会怎么改变,要怎么让它按你的需求去变或者不变,这些问题都需要你对 launchMode 有足够的了解;而且不止 launchMode,这是一个以 Activity 的回退栈为核心的大话题,你把这个大话题弄明白了,才可以指哪打哪随心所欲。

<activity> 的 android:launchMode
standard
singleTop
singleTask
singleInstance

startActivity() 时的 Intent.FLAG_ACTIVITY_XXX
FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_SINGLE_TOP
FLAG_ACTIVITY_CLEAR_TOP
FLAG_ACTIVITY_MULTIPLE_TASK
FLAG_ACTIVITY_NEW_DOCUMENT
FLAG_ACTIVITY_PREORDER_TO_FRONT
FLAG_ACTIVITY_PREVIOUS_IS_TOP
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
FLAG_ACTIVITY_RETAIN_IN_RECENTS
FLAG_ACTIVITY_TASK_ON_HOME

<activity> 的 android:taskAffinity
<activity> 的 android:allowTaskReparenting
<activity> 的 android:clearTaskOnLaunch
Activity 的回退栈(TaskAndroid 的最近任务列表(Recents/Overview)切换
启动器(桌面)的 app 图标点击
....

面试官有时侯会问一些比较刁钻的 launchMode 的问题,其实也不是为了刁难你,这些问题对实际开发都是有用的,只是它比较难掌握而已。所以现在就把 launchMode 以及跟它相关的这一大套东西给大家讲清楚。

在这里插入图片描述
当我们点击了手机的最近任务键的时候,我们看到的是一个个的 App?一个个的 Activity?我们看到的是一个个的任务(Task)

在这里插入图片描述
当我们的 App 图标在桌面被点击的时候,App 默认的 Activity,也就是那个配置了 MAIN+LAUNCHER 的 intent-filer 的 Activity 将会被启动,并且这个 Activity 会被放进系统刚创建的一个 Task 里。

我们通过最近任务键可以在多个 app 之间进行切换,但其实更精确的说,我们是在多个 Task 之间切换

在这里插入图片描述
每个 Task 都有一个自己的回退栈,它按顺序记录了用户打开的每个 Activity ,这样就可以在用户按返回键的时候,按照倒序来依次关闭这些 Activity。

当回退栈里最有一个 Activity 被关闭,这个 Task 的生命也就结束了。

在这里插入图片描述
但它并不会在最近任务列表里消失,系统依然会保留这个 Task 的残影给用户,目的是让用户可以方便的【切回去】,只是这种时候 所谓的【切回去】,其实是对 app 的重新启动。因为原先的那个 Task 已经不存在了。所以在最近任务里看见的 Task,未必是活着的

在这里插入图片描述
Activity 是一个可以跨进程、跨应用的组件,当你在 A App 的 Activity 打开 B App 的 Activity 的时候,这个 Activity 会被直接放进 A 的 Task 里,而对于 B App 的 Task 是没有任何影响的。

为什么没有任何影响?为什么这么设计?

首先我们想一想,我们为什么要打开别的 App 的 Activity?因为它提供了一个通用的功能。

比如通讯录 App,可能会提供一个添加联系人的 Activity 供其他 App 使用,那么这些通用的功能,它的逻辑是和谁相关的?

在这里插入图片描述
比如我从短信 App 里点击一个电话号码,选择【新建联系人】,然后通讯录 App 提供的添加联系人 Activity 就会被打开。

这个 Activity 它的逻辑是和哪个 App 相关的?它和短信 App 相关的,因为它就是从短信 App 跳过来的,它们是在一整个逻辑链条上的。如果我现在按了返回键,我会回到刚才的短信界面。

在这里插入图片描述
它和通讯录 App 是不相关的。所谓不相关,就是这时候如果用户按下最近任务键,它不应该看到通讯录的 Task;而它现在回到桌面点击通讯录 App 的图标,它看到的也不应该是这个添加联系人的界面,而应该是一个联系人列表,因为用户的这个操作,大概率是要查看通讯录。

在这里插入图片描述
相反,在这个时候他在切回短信 App,他应该回到刚才的添加联系人页面,继续编辑联系人信息。

所以对于【添加联系人】这个页面来说,它是和打开它的那个 App 有相关性,而不是提供它的 App。更确切的说,也不是和打开它的 App 相关,而是和打开它的 Task 相关。

而这个逻辑实际上也是 android 默认的规则:在不同 Task 中打开同一个 Activity,Activity 会被创建出不同的实例,分别放进每一个 Task 互不干扰。这是符合产品逻辑也是符合用户心理的。

但是,这只是默认的规则,有的时候我们会需要不同的产品逻辑。

在这里插入图片描述
比如我在短信 App 里点击的不是电话号码,而是一个邮箱地址,那么我的邮箱 App 提供的编写邮件的 Activity 就会被打开。这个时候这个编写邮件的 Activity 它的逻辑是和哪个 App 相关的?

在这里插入图片描述
首先,它是和短信 App 相关的,原因跟刚才一样,它是从短信 App 打开的,那么它和邮箱 App 相关吗?也是相关的,因为按照一般人使用邮件的习惯,如果现在按下最近任务键,用户会期望看到邮箱 App 的 Task 出现在短信 Task 的旁边,并且当它点击这个 Task 或者切回桌面点击邮箱 App 的图标,他都会期望回到写邮件的界面继续写。

编写邮件和添加联系人这两件事并没有本质的不同,只是用户不同的心理预期决定了决定了我们要有不同的产品逻辑。

所以如果你们也做通讯录或者邮箱而且产品逻辑和我说的不一样这都没关系,因为这是产品经理负责的事,我在说的是如果你有怎样的产品逻辑你应该怎么写。

那么如果我要做这种逻辑的邮箱,我应该怎么办呢?很简单,只要在 AndroidManifest.xml 里把编写邮件的 <activity> 的 launchMode 设置为 singleTask 就行了。

在这里插入图片描述
singleTask 可以让 Activity 被别的 Activity 启动的时候不会进入启动它的 Task 里,而是会在属于它自己的 Task 里创建,放在自己的栈顶,然后把这整个 Task 一起拿过来压在启动它的 Task 的上面

这个逻辑可以保证不管是从哪个 App 启动,被标记为singleTask 的 Activity 总会被放在自己的 Task 里。

在这里插入图片描述
如果你仔细留意也会发现,这种方式 打开的 Activity 的入场动画是应用间切换的动画,而不是普通的 Activity 入场动画

这种不一致并不是 Android 不拘小节不修边幅,相反这是在刻意的提醒用户你正在进行跨任务操作。

在这里插入图片描述
这时候如果用户点返回键,界面会显示你的 App 里的上一个 Activity,而不是直接返回到之前的 App。直到用户反复按返回键把这个 App 所有的 Activity 全部关闭了,上面的 Task 消失下面的 Task 才会出来。也就是对于我们的例子来说,短信 App 才会露出来,而且这次又变成了应用间切换的动画,确切的说是 Task 间切换的动画。

也就是说,不止 Activity 在 Task 内部可以叠成栈,不同 Task 之间也可以叠起来

不过有一点,Task 的叠加只适用于前台 Task,前台叠加的多个 Task 在进入后台的第一时间就会被拆开

前台 Task 进入后台最常见的场景有两种:

  • 按 Home 键回到桌面

  • 按最近任务键查看最近任务

需要注意的是,前台 Task 在最近任务列表显示出来的时候就已经进入后台,而不是在切换到其他应用之后

在这里插入图片描述
所以如果用户从短信 App 进入邮箱 App 之后没有直接按返回键,而是先查看一下最近任务列表,再马上切回去,这个时候虽然表面上看着没变,但实际上前台 Task 已经只剩下一个了。

在这里插入图片描述
现在如果用户再连续按返回键关掉邮件 App 的 Task,他就不会回到短信 App 了而是直接回到桌面。

我觉得这个其实有点反用户直觉的,我只是切出去再切回来怎么就变了?但是 Android 就是这么工作的。

除了 singleTask,对于新建邮件这种场景还有一个解决方案,是使用一个 allowTaskReparenting 的属性,Activity 在默认情况下只会归属于一个 Task,不会在多个 Task 之间跳来跳去,但你可以通过设置来改变这个逻辑。

在这里插入图片描述
如果你不是用 singleTask 来设置编写邮件的 Activity,而是把它的 allowTaskReparenting 属性设置为 true,那么当用户从短信 App 打开这个编写邮件 Activity 的时候,它虽然依然会进入短信 App 的 Task 里,但当稍后用户再从桌面点开邮件 App 的时候,原先放在短信 Task 里的 Activity 会被挪过来放进邮件 App 的 Task 里,在回退栈的顶端被显示出来。

在这里插入图片描述
而这时候你再切回短信,你会发现那个 Activity 已经不见了。

这也就是 所谓的【Task Reparenting】,你打开我的时候我在你的 Task 里,而稍后我也可以回到我原本所属的 Task 来

在这里插入图片描述
这跟 singleTask 比起来,因为 Activity 刚被打开的时候并没有发生 Task 切换,所以也没有 Task 切换的夸张的入场动画,对于用户是无感知的,而且因为只有一个 Task,用户切到后台再切回来的时候,也不会像 singleTask 那样被切断自己的回退路径。

不过从 Android 9 开始这个属性失效了,不知道是不是因为这个属性用的人太少了导致 android 团队把这个属性改坏了也没发现就这么发布出来了;而在最新的 Android 11 上这个属性又被修好了又能正常工作了。总之,这个属性的设计是很好的,但 它在 Android 9 和 10 的系统上是坏的,所以这个 allowTaskReparenting 虽然很好用,但如果你要用请做好测试以及各种心理准备

singleTask 除了保证 Activity 在固定的 Task 里创建,还有一个行为规则:

在这里插入图片描述
如果启动的时候这个 Task 的栈里已经有了这个 Activity,那么就不再创建新的对象,而是直接复用这个已有的对象。同时,因为 Activity 没有被重建,系统也就不会调用它的 onCreate 方法,而是调用它的 onNewIntent 方法,让它可以从 Intent 里解析数据来刷新界面。

在这里插入图片描述
另外在调用 onNewIntent 之前,如果这个 Activity 的上面压着有其他的 Activity,系统也会把这些 Activity 全部清掉,来确保我们的 Activity 出现在栈顶。

这样 singleTask 其实是既保证了【只有一个 Task 里有这个 Activity】,又保证了【这个 Task 里最多只有一个这个 Activity】

所以虽然它的名字叫 singleTask,但实质上限制了它所修饰的 Activity 在全局只有一个对象。singleTask 其实是个事实上的全局单例。

除了 singleTask,Android 还提供了一种更彻底的 launchMode 的选项:singleInstance

singleInstance 它的行为逻辑和 singleTask 是基本一致的,只是它多了一个更严格的限制:它要求这个 Activity 所在的 Task 里只有这么一个 Activity,下面没有旧的,上面也不许有新的

在这里插入图片描述
具体来说,比如我把编写邮件的 Activity 设置成了 singleInstance,那么当用户在短信 App 里点击了邮箱地址之后,邮件 App 不仅会创建这个 Activity 的对象,而且会创建一个单独的 Task 来把这个 Activity 放进去。

在这里插入图片描述
或者如果之前已经创建过这个 Task 和 Activity,那么就会像 singleTask 一样,直接复用这个 Activity 调用它的 onNewIntent 方法。另外,这个 Task 也会被拿过来压在短信 Task 的上面,入场动画是切换 Task 的动画。

在这里插入图片描述
这时候如果用户点击返回,上面的 Task 因为只有一个 Activity,所以手机会直接回到短信 App,出场动画也是切换 Task 的动画。

在这里插入图片描述
而如果用户没有直接点击返回,而是先看了一下最近任务又返回来,这时候因为下面的短信的 Task 已经被推到后台,所以用户再点返回的话,就会回到桌面而不是回到短信 App。

在这里插入图片描述
而用户既没有点击返回也没有切后台,而是在编写邮件的 Activity 里又启动了新的 Activity,那么由于 singleInstance 的限制,这个新打开的 Activity 并不会进入当前的 Task,而是会被装进另一个 Task 里,然后随着这个 Task 一起被拿过来压在最上面。

这就是 singleInstance 和 singleTask 的区别。

singleTask 强调的只有唯一性,我只会在一个 Task 里出现,而且这个 Task 里面也只有一个我的实例;而 singleInstance 除了唯一性,还要求独占性,我要独自霸占一个完整的 Task

那么在实际的操作中,它们的区别就是在被启动之后,用户按返回键时,singleTask 会在自己的 App 里进行回退,而 singleInstance 会直接回到原先的 App。

在这里插入图片描述
以及用户稍后从桌面点开 Activity 所在的 App 的时候,singleTask 会看到这个 Activity 依然在栈顶,而 singleInstance 会看到这个 Activity 已经不见了!

它在哪?它并没有被杀死,而是在后台的某个地方默默蹲着,当你再次启动它,它就会再次跑到前台来,并被再得到一次 onNewIntent 的回调。

刚在说到,在最近任务里看见的 Task 未必还活着,那么这里就可以再加一句,在最近任务里看不见的 Task 也未必就死了,比如 singleInstance

那既然它活着,为什么会被藏起来呢?因为它们的 taskAffinity 冲突了。

在 Android 里,一个 App 默认最多只有一个 Task 可以显示在最近任务列表里,但其实用来甄别这个唯一性的并不是 App,而是一个叫做 taskAffinity 的东西

Affinity 就是相似、有关联的意思,在 Android 里每个 Activity 都有一个 taskAffinity,它就相当于是对每个 Activity 进行的一个预先的分组,它的值默认取自 Activity 所在的 Application 的 taskAffinity,而 Application 的 taskAffinity 默认取自 App 的包名

另外,每个 Task 也有它的 taskAffinity,它的值取自栈底的 Activity 的 taskAffinity。我们可以通过 AndroidManifest.xml 来定制 taskAffinity,但是 默认情况下,一个 App 里面所有 Task 它们的 taskAffinity 都是一样的,就是 App 的包名

在这里插入图片描述
当我们启动一个新的 Task 的时候,比如开机后初次点开一个 App,这个 Task 就会得到一个 taskAffinity,它的值就是它所启动的第一个 Activity 的 taskAffinity。

在这里插入图片描述
当我们继续从已经打开的 Activity 再次打开新的 Activity 的时候,taskAffinity 就会被忽略了,新的 Activity 会直接入栈,不管它来自哪。

  • 默认情况下,Activity 会直接进入当前 Task

  • 对于配置了 launchMode=singleTask 的 Activity,系统会对比 Activity 和当前 Task 的 taskAffinity 是否相同

    • 如果相同,依然正常入栈

    • 如果不同,Activity 会去寻找和它的 taskAffinity 相同的 Task 后入栈

    • 如果找不到,系统就为它创建一个新的 Task

在这里插入图片描述
所以当你在 App 里启动一个配置了 singleTask 的 Activity,如果这个 Activity 来自别的 App,就会发生 Task 的切换。

在这里插入图片描述
而如果这个 Activity 是你自己 App 里的,你会发现它进入了当前 Task 的栈顶。因为这种情况下,新 Activity 和当前 Task 的 taskAffinity 是相同的。

在这里插入图片描述
而如果你再给这个 Activity 设置一个独立的 taskAffinity,你又会发现,哪怕是同一个 App 这个 Activity 也会被分拆到另一个 Task 里。

在这里插入图片描述
而且如果这个独立设置的 taskAffinity 恰好和另一个 App 的 taskAffinity 一样,这个 Activity 还会直接进入别人的 Task 去。

  • 最近任务列表会列出现有的 Task

    • 但它们的 taskAffinity 需要不一样

    • 当多个 Task 具有相同 taskAffinity 的时候,最近任务列表里只会显示最新展示过的那一个

所以也就能说明为什么设置了launchMode 为 singleInstance 的 Activity 不会显示在最近任务列表里。

launchMode 除了刚才讲的默认的也就是 standard,还有 singleTask 和 singleInstance 之外,还有一种叫做 singleTop

singleTop 虽然在名字上有一个 single,但它在关系上和默认的 standard 其实更近一些:

  • 和默认的(即 standard)几乎一样,启动时不考虑 taskAffinity

  • 唯一的区别是,如果启动的Activity 已经在栈顶,singleTop 会重用栈顶的 Activity 回调 onNewIntent

简单来说,默认的 standard 和 singleTop 是直接摞在当前的 Task 上,而 singleTask 和 singleInstance 则是两个【跨 Task 打开 Activity】的规则;虽然也不一定跨 Task,但它们的行为规则展现出了很强的跨 App 交互的意图

实战的通常选择:

  • 默认 standard 和 singleTop:多用于 App 内部

  • singleInstance:多用于开放给外部 App 来共享使用

  • singleTask:内部交互和外部交互都会用得上

至于具体用谁,就得根据需求具体分析了。

讲了这么多,其实一直都在围绕任务启动和任务切换的问题,瞄准的就是更加精准可控的界面导航。如果记不全,Task 的工作模型一定要记住,这是最核心最重要的。它能让你站在一个更高的高度去理解 Android 的 Activity 启动和任务切换,对工作会非常有帮助。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值