厂商动态 > 正文

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

2020-10-17 11:13:42来源:

巅峰赘婿

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

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

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

插图:

  • 的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以及和它相关的这一大套东西,给大家讲清楚。安全带系好了。

视频先行

视频版本在这里:

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

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

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

本期视频用了大量的3D动画来配合讲解,比如这样:

所以有条件的话强烈建议观看视频版本,因为本期的文字版可能会比较不适合阅读。

下面的文字是本期视频的脚本,为了方便阅读才修改成了文章的格式。所以如果你点开视频,下面的文字就不用看了。Task和回退栈

大家好,我是扔物线朱凯。

先问个问题:当我们在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是一个可以跨进程、跨应用的组件。当你在AApp里打开BApp的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已经不见了。这也就是所谓的「TaskReparenting」。你打开我的时候,我在你的Task里;稍后我又可以回到我原本所属的Task来。

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

好用吧?

不过!很恶心是,我发现从Android9开始,这个属性失效了!不知道是不是因为这个属性用的人太少了,导致Android团队把这个属性改坏了也没发现,就这么发布出来了。(冷笑——摔手机。)而且我还发现,在最新的Android11上,这个属性又被修好了,工作正常了!总之,这个属性的设计是很好的,但它在Android9和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技能,快速升级、快速跳槽提薪,我的系列化课程应该会更适合。扫码加我的小助理,可以免费领试听课。

那么今天的内容就到这里,大家喜欢的话别忘了三连和转发,让更多需要的人看到。我是扔物线,我不和你比高低,我只助你成长,我们下期见。

来源:搜狐

  • 电影花絮
  • 电影情报
  • 圈子新闻
  • 电影新闻
  • 电影搜罗
  • 电视剧
  • 影视演员
推荐阅读