Android 面试黑洞——当我按下 Home 键再切回来,会发生什么?

不少 Android 工程师在投简历找工做以前,会去补习一下 Activity 的启动模式(launchMode),由于面试的时候常常会考。但真正把它搞懂的人是不多的——包括很多拿它作面试题的面试官。

就像我在视频标题里说的,当用户在使用 App 的时候按下了 Home 键,而后再切回来,或者在多个 App 之间切来切去,App 的内容会不会改变、会怎么改变、要怎么让它按你的需求去变或不变,这些问题都须要你对 launchMode 有足够的了解。并且不仅是 launchMode,这是一个以 Activity 的回退栈(Back Stack)为中心的大话题。android

插图:面试

  • 的 launchMode:
    • standard
    • singleTop
    • singleTask
    • singleInstance
  • Intent.FLAG_ACTIVITY_***
    • FLAG_ACTIVITY_NEW_TASK
    • FLAG_ACTIVITY_SINGLE_TOP
    • FLAG_ACTIVITY_CLEAR_TOP
    • FLAG_ACTIVITY_MULTIPLE_TASK
    • FLAG_ACTIVITY_NEW_DOCUMENT
    • FLAG_ACTIVITY_REORDER_TO_FRONT
    • FLAG_ACTIVITY_PREVIOUS_IS_TOP
    • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
    • FLAG_ACTIVITY_RETAIN_IN_RECENTS
    • FLAG_ACTIVITY_TASK_ON_HOME
  • 的 android:taskAffinity
  • 的 android:allowTaskReparenting
  • 的 android:clearTaskOnLaunch
  • Activity 的回退栈(Task)
  • Android 的最近任务列表(Recents / Overview)切换
  • 启动器(桌面)的 App 图标点击
  • ……


你把这个大话题弄明白了,才能够指哪打哪,为所欲为。面试官有时候问一些比较刁钻的 launchMode 的问题,其实也不是为了刁难你,这都是对实际开发有用的,只是它比较难掌握而已。

因此今天,我就把 launchMode 以及和它相关的这一大套东西,给你们讲清楚。安全带系好了。 安全

视频先行

要看视频的能够直接去 哔哩哔哩 或者 YouTube 观看。微信

强烈建议扫码看视频版本!
markdown

强烈建议扫码看视频版本!
ide

强烈建议扫码看视频版本!
oop

本期视频用了大量的 3D 动画来配合讲解,好比这样: 微信图片_20201015144036.gif 因此有条件的话强烈建议观看视频版本,由于本期的文字版可能会比较不适合阅读。测试


下面的文字是本期视频的脚本,为了方便阅读才修改为了文章的格式。因此若是你点开视频,下面的文字就不用看了。
动画

Task 和回退栈

你们好,我是扔物线朱凯。spa

先问个问题:当咱们在 Android 手机里点了最近任务的方块键,咱们看到的这是一个个的……什么?


一个个…… Activity?一个个…… App?咱们看到的是一个个……Task,任务。

当咱们的 App 图标在桌面上被点击的时候,App 的默认 Activity——也就是那个配置了 MAIN + LAUNCHER 的 intent-filter 的 Activity——会被启动,而且这个 Activity 会被放进系统刚建立的一个 Task 里。咱们经过最近任务键能够在多个 App 之间进行切换,但其实更精确地说,咱们是在多个 Task 之间切换。

每一个 Task 都有一个本身的回退栈,它按顺序记录了用户打开的每一个 Activity,这样就能够在用户按返回键的时候,按照倒序来依次关闭这些 Activity。当回退栈里最后一个 Activity 被关闭,这个 Task 的生命也就结束了。


但它并不会在最近任务列表里消失。系统依然会保留这个 Task 的一个残影给用户,目的是让用户能够方便地「切回去」;只是这种时候的所谓「切回去」,实际上是对 App 的从新启动,由于原先的那个 Task 已经不存在了。

因此,在最近任务里看见的 Task,未必是还活着的。

singleTask

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


为何?为何这么设计?

首先咱们想想:咱们为何要打开别的 App 的 Activity?由于它提供了一个通用的功能,对吧?好比通信录 App 可能会提供一个添加联系人的 Activity 供其余 App 使用。那么这些通用的功能,它的逻辑是和谁相关的?好比我从短信 App 里点击一个电话号码,选择「新建联系人」,而后通信录 App 提供的添加联系人 Activity 就会被打开,对吧?这个 Activity 它的逻辑是和哪一个 App 相关的?和短信相关吗?相关的,由于它是从短信跳过来的嘛,它们是在一整个逻辑链条上的。换句话说,若是我如今按了返回键,我会回到刚才的短信界面。是吧?那它和通信录相关吗?是不相关的。所谓不相关,就是在这个时候用户若是按下最近任务的方块键,他不该该看到通信录的 Task;而若是他如今回到桌面,点击通信录的图标,他看到的也不该该是这个添加联系人的页面,而应该是一个联系人列表,由于用户的这个操做大几率是要查看通信录;相反,在这个时候他再切回短信 App,他应该回到刚才的添加联系人页面,继续编辑联系人信息。因此对于「添加联系人」这个页面来讲,它是和打开它的那个 App 有相关性,而不是和提供它的 App,对吧?更确切地说,也不是和打开它的 App 相关,而是和打开它的 Task 相关。是这回事吧?而这个逻辑,实际上也是 Android 默认的规则。当你在不一样的 Task 里打开相同的 Activity 的时候,这个 Activity 会被建立出不一样的实例,分别放在每个 Task 里,互不干扰。这是符合产品逻辑,也是符合用户心理的。

可是!这只是默认的规则。有的时候咱们会须要不一样的产品逻辑。好比我在短信里点击的不是电话号码,而是一个邮箱地址,那么个人邮箱 App 提供的编写邮件的 Activity 就会被打开,对吧?这个时候,这个编写邮件的 Activity,它的逻辑是和哪一个 App 相关的?首先,依然是和短信 App 相关的,对吧?缘由跟刚才同样,它是从短信打开的。那么它和邮箱 App 相关吗?也是相关的。由于按照用户使用邮件的习惯,若是如今按下最近任务键,用户会指望看到邮箱 App 的 Task 出如今短信 Task 的旁边,而且当它点击这个 Task,或者当它切回桌面点击邮箱 App 的图标,他都会指望回到写邮件的界面继续写。编写邮件和添加联系人这两件事并无本质的不一样,只是用户不一样的心理预期决定了咱们要有不一样的产品逻辑。因此若是大家也作通信录或者邮箱,并且产品逻辑和我说的不同,不要紧,这是产品经理负责的事,我在说的是若是你有怎样的产品逻辑,你应该怎么写。

那么若是我要作这种逻辑的邮箱,我应该怎么办呢?很简单,只要在 AndroidManifest.xml 里把这个编写邮件的 的 launchMode 设置为 singleTask 就好了。


singleTask 可让 Activity 被别的 App 启动的时候不会进入启动它的 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 是在显示最近任务的时候就已经进入了后台,而不是在你切换到其余应用以后。因此若是用户从短信进入邮箱之后没有按直接返回键,而是先查看一下最近任务再立刻按返回键切回去,这个时候虽然表面上看着没变,但实际上前台 Task 已经只剩下了一个。如今若是用户再连续按返回键关掉邮件 App 的 Task,他就不会回到短信了,而是直接回到桌面。


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

allowTaskReparenting

除了 singleTask,对于新建邮件这种场景,还有一种解决方案是使用一个叫作 allowTaskReparenting 的属性。Activity 默认状况下只会归属于一个 Task,不会在多个 Task 之间跳来跳去,但你能够经过设置来改变这个逻辑。若是你不是用 singleTask 来设置编写邮件的 Activity,而是把它的 allowTaskReparenting 属性设置为 true,那么当用户从短信里打开这个 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 的手机上是坏的——我还专门拿个人三星 S20 也测试了一下,确认了三星也没有修复这个问题。可是用户可不会怪手机,更不会怪系统,他们只会怪你的 App 难用。因此这个 allowTaskReparenting,虽然很好用,但若是你要用,请作好测试以及各类心理准备。

singleInstance

singleTask 除了保证 Activity 在固定的 Task 里建立,还有一个行为规则:若是启动的时候这个 Task 的栈里已经有了这个 Activity,那么就再也不建立新的对象,而是直接复用这个已有的对象;同时,由于 Activity 没有被重建,系统也就不会调用它的 onCreate() 方法,而是调用它的 onNewIntent() 方法,让它能够从 Intent 里解析数据来刷新界面(若是须要的话);另外在调用 onNewIntent() 以前,若是这个 Activity 的上面压着的有其余 Activity,系统也会把这些 Activity 所有清掉,来确保咱们要的 Activity 出如今栈顶。

那么这样 singleTask 实际上是既保证了「只有一个 Task 里有这个 Activity」,又保证了「这个 Task 里最多只有一个这个 Activity」,因此虽然它名字叫 singleTask,但它在实质上限制了它所修饰的 Activity 在全局只有一个对象。

在 singleTask 以外,Android 还提供了一种更完全的 launchMode 的选项:singleInstance。

刚才我说,singleTask 实际上是个事实上的全局单例,是吧?那这个 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

那既然它还活着,为何会被藏起来呢?由于它们的 taskAffinity 冲突了。

在 Android 里,一个 App 默认只能有一个 Task 显示在最近任务列表里。但其实用来甄别这份惟一性的并非 App,而是一个叫作 taskAffinity 的东西。Affinity 就是类似、有关联的的意思,在 Android 里,每一个 Activity 都有一个 taskAffinity,它就至关因而对每一个 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 被配置了 singleTask,Android 就会去检查新的 Activity 和当前 Task 的 taskAffinity 是否是相同,若是相同就继续入栈,而若是不一样,新 Activity 就会进入和它本身的 taskAffinity 相同的 Task,或者建立一个新的 Task。

因此当你在 App 里启动一个配置了 singleTask 的 Activity,若是这个 Activity 来自别的 App,就会发生 Task 的切换;而若是这个 Activity 是你本身 App 里的,你会发现它直接进入了当前 Task 的栈顶,由于这种状况下新 Activity 和当前的 Task 的 taskActivity 是相同的。而你若是再给这个 Activity 设置一个独立的 taskAffinity,你又会发现,哪怕是同一个 App,这个 Activity 也会被分拆到另外一个 Task 里。并且若是这个独立设置的 taskAffinity 刚好和另外一个 App 的 taskAffinity 同样,这个 Activity 还会直接进入别人的 Task 去。

当咱们查看最近任务的时候,不一样的 Task 会并列展现出来,但有一个前提:它们的 taskAffinity 须要不同。在 Android 里,同一个 taskAffinity 能够被建立出多个 Task,但它们最多只能有一个显示在最近任务列表。这也就是为何刚才例子里 singleInstance 的那个 Activity 会从最近任务里消失了:由于它被另外一个相同 taskAffinity 的 Task 抢了排面。

说到这儿,有一点须要注意,Android 的官方文档在 launchMode 方面的描述有不少的错误和自相矛盾。好比官方文档里说 singleTask 「只会出如今栈底」,但其实彻底没有这回事。咱们在官方文档里看到的错误通常是什么呢:错别字,或者有歧义、有误导性的表达。可是这个错误说实话让我有点莫名其妙,就是你根本无法猜出来写文档的人的本来想表达的是什么意思,给个人感受就跟造谣似的。总之你若是在官方文档里看到一些和你的测试结果不符的描述,以你的测试为准;或者若是你发现它有一些话自相矛盾,你就当它没说。

singleTop

launchMode 除了刚才讲的默认的——也就是 standard——和 singleTask 以及 singleInstance 以外,还有一种叫作 singleTop。singleTop 虽然名字上也带有一个 single,但它的关系和默认的 standard 其实更近一些。它和默认同样,也是会直接把 Activity 建立以后加入到当前 Task 的栈顶,惟一的区别是:若是栈顶的这个 Activity 刚好就是要启动的 Activity,那就不新建了,而是调用这个栈顶的 Activity 的 onNewIntent()。

简单说来就是,默认的 standard 和 singleTop 是直接摞在当前的 Task 上;而 singleTask 和 singleInstance 则是两个「跨 Task 打开 Activity」的规则,虽然也不是必定会跨 Task,但它们的行为规则展示出了很强的跨 App 交互的意图。在实战上,咱们会比较多地在 App 内部使用默认和 singleTop;singleInstance 会比较多用于那些开放出来给其余 App 一块儿用的共享 Activity;而 singleTask 则是个兼容派,内部交互和外部共享都用得着。至于具体用谁,就要根据需求具体分析了。

总结

讲了这么多,其实一直都在围绕任务启动和任务切换的问题,瞄准的就是更精准可控的界面导航。若是记不全,Task 的工做模型必定要记住,这是最核心最重要的。别的你均可以忘,这个模型必定记清楚了,这能让你站在一个更高的高度去理解 Android 的 Activity 启动和任务切换,对工做会很是有帮助,并且这些内容是你不管在网上现有的博客仍是官方文档里都很难看到的。 至于更多的细节,好比这些启动模式的一些坑,Intent 的 FLAG_ACTIVITY_ 打头的 Flag,以及 AndroidManifest.xml 里更多的配置参数,我就不一一细讲了。只要你把我今天说的 Task 的工做模型搞清楚,再把刚才讲的这四种 launchMode 想明白,那些细节很容易就能够掌握。学技术,就是要学到本质,以不变应万变。 若是你实在连这最后一步也懒得研究,就是想躺着把各类细节都学了,来我知识星球吧,全都有。 另外若是你想全方位提高本身的 Android 技能,快速升级、快速跳槽提薪,个人系列化课程应该会更适合。 那么今天的内容就到这里,你们喜欢的话别忘了三连和转发,让更多须要的人看到。我是扔物线,我不和你比高低,我只助你成长,咱们下期见。

相关文章
相关标签/搜索