×

也聊聊activity的launchMode启动模式

96
sunhapper
2018.03.21 22:59* 字数 3160

activity启动模式是android开发中的基础知识,面试中也经常被问到,不过刚入门的时候看到网上众多介绍activity启动模式的文章,看过一遍感觉都懂了,实际碰到具体的场景有时候还是有点懵逼。

现在对这块的知识用的多了,也算有了一些心得,尝试着用简单的方式总结备忘下。

这篇文章主要是对于activity启动模式的相关知识的一个规律总结,基础的知识点会介绍的比较简略,适合已经大致了解了activity的启动模式,但对于具体场景分析还不太熟练的同学,看完这篇文章你应该就能按照一个既定的套路去分析activity的启动模式了

基础知识点

Task

  • 一个后进先出的栈结构,用来保存调度activity

启动模式

standard

默认的启动模式

  • 默认只在当前task启动
  • 启动时会新建一个activity实例放到栈顶

singleTop

栈顶复用模式

  • 默认只在当前task启动
  • 当当前task的栈顶的已经有一个相同的Activity在栈顶了,则不会创建新的activity,而是会调用栈顶activity的onNewIntent()方法
  • 栈顶不存在相同Activity时,会新建一个activity实例放到栈顶

singleTask

  • 默认可以在其他task启动,但是到底会不会在其他task启动需要依赖taskAffinity具体分析
  • 当当前task中已经存在一个相同的Activity时,会将task中位于该Activity实例上方的activity出栈直到该Activity实例位于栈顶(类似Intent中添加FLAG_ACTIVITY_CLEAR_TOP的效果)
  • 对一个singleTask的Activity类,系统中只会存在一个实例

singleInstance

独享task

  • 默认新建task,并独占该task
  • singleInstance的Activity中启动的task会在其他task
  • 系统中同样只会存在一个实例

taskAffinity

个人简单理解为根activity的taskAffinity可以决定task的“名字”,activity在新task启动时和re-parent时需要根据taskAffinity来确定该activity会出现在哪个task

FLAG

除了在AndroidManifest.xml中指定launchMode之外,在设置Intent的FLAG同样会影响到activity的启动,这里只介绍下FLAG_ACTIVITY_NEW_TASK,其他的FLAG文章中暂时不会涉及,以后有空再补

FLAG_ACTIVITY_NEW_TASK

网上很多文章说在Intent中设置了FLAG_ACTIVITY_NEW_TASK相当于为Activity指定了singleTask模式,但是实际上singleTask模式具有三个特点

  • 可以在其他task启动
  • 会复用task中已有的Activity实例,并将其上的activity出栈
  • singleTask的activity系统中只会存在一个实例

设置FLAG_ACTIVITY_NEW_TASK只是让一个activity具有了在其他task启动的能力,并没有后面两个特点,所以上面的说法其实是不准确的

FLAG_ACTIVITY_NEW_TASK

  • 可以让standard/singleTop的activity具备在其他task启动的能力
  • 不会改变singleTask/singleInstance的activity行为
  • 使用非Activity的context.startActivity(),需要在Intent中设置FLAG_ACTIVITY_NEW_TASK,否则会报错

本文涉及到的基础知识就简单介绍完了,下面开始进入正题

activity启动规律总结

为了表述方便,后面统一用CurrentActivity和TargetActivity表示一个activity启动另一个activity的场景

把singleInstance拉出来单独说一下

  • 当CurrentActivity的启动模式是singleInstance时,因为它独占了当前task,TargetActivity不论启动模式是什么都会在其他task启动
  • 当TargetActivity是singleInstance时,它会在其他task中启动并独占这个task
  • 后面启动规律说明中都不包含CurrentActivity的启动模式是singleInstance这个特殊的情况,这个大家注意下

首先判断TargetActivity是否只能在当前task启动

先列个表格

TargetActivity是否可以在新task中启动 standard singleTop singleTask singleInstance
设置FLAG_ACTIVITY_NEW_TASK true true true true
不设置FLAG_ACTIVITY_NEW_TASK false false true true

一句话总结:singleTask/singleInstance的activity本身具有在新task中启动的能力,standard/singleTop的activity要想拥有在新task中启动的能力,需要在设置Intent.FLAG_ACTIVITY_NEW_TASK

第二步判断TargetActivity所在的task

总共三种情况:

  • 在当前task启动
  • 新建一个task并创建activity
  • 将已有的task切换到前台并启动activity

对于不具有在新task中启动能力的activity

未指定Intent.FLAG_ACTIVITY_NEW_TASK
standard/singleTop的activity,对不起,你没有这个能力,只能老老实实在当前task中启动。

当然如果当前应用一个task都没有,那么这个activity启动时会创建一个task,这个activity的实例作为task的根activity。

对于具有在新task中启动能力的activity

singleTask/singleInstance以及在Intent中设置了FLAG_ACTIVITY_NEW_TASK的standard/singleTop具有在新task中启动能力。

至于是否在一个新task中启动,还要受其他条件的限制,这个条件可以先简单的认为是taskAffinity

  • 当前task根activity的taskAffinity和TargetActivity的taskAffinity相同时TargetActivity会在当前task中启动
  • 前一种情况不满足时,如果已经存在了一个task的根activity的taskAffinity和TargetActivity的taskAffinity相同,则将这个task切到前台并启动TargetActivity
  • 以上两种情况都不满足,会新建一个task,TargetActivity的实例作为根activity,这个task的taskAffinity由TargetActivity决定

总结一下就是找一个taskAffinity的task去启动,找不到就新建一个(这里会忽略了singleInstance独占的task)

默认的taskAffinity

不指定taskAffinity的话默认是应用的包名,也可以为在application中指定所有的activity的taskAffinity。

优先级是activity中指定的taskAffinity>application中指定的taskAffinity>默认的包名

这里需要特别注意下,不少文章中说singleTask的TargetActivity想要在新的task中启动需要设置taskAffinity,其实真正原因是不设置的话,所有activity都用的默认的,也就是同一个应用中taskAffinity不指定的话默认都是相同的,按照上面的分析singleTask的TargetActivity肯定会在当前task启动的。

如果设置MainActivity或者说当前task的根activity的taskAffinity为一个不同的值话,不指定taskAffinity的singleTask的TargetActivity同样会在新的task中启动。

最后第三步根据TargetActivity的启动模式判断会如何启动

  • 新建task的场景简单,创建TargetActivity实例作为根activity
  • standard:在栈顶创建TargetActivity实例
  • singleTop:task栈顶是一个已经存在的TargetActivity的实例的话,复用这个activity,不创建新的实例;否则在栈顶创建新的TargetActivity的实例
  • singleTask:如果task中已经存在TargetActivity的实例,那么会将其上的所有activity出栈,并复用这个activity,不创建新的实例;反之则创建新的TargetActivity的实例

套路总结

  • singleInstance独享task,单独拎出来看
  • 剩下的分为两类:只能在当前task中启动的/可以在其他task中启动的activity
  • 对于可以在其他task中启动的activity,找根activity的taskAffinity与之相同的task,找到就确定TargetActivity会出现在这个task中,找不到就新建一个task,创建TargetActivity实例作为根activity
  • 确定了task,再根据不同启动模式的特点分析这个task中activity的情况

返回的情况

返回的情况暂时只考虑按返回键的场景,而不考虑在系统的最近任务的中切换后的情况(以后有空单独写篇文章)

只考虑按返回键的场景其实比较简单,只需要了解总是

  • 先将前台的task栈顶的activity出栈
  • 直到前台task被清空再将后台的task切换到前台继续第一步操作

异常的场景

按照上面的思路分析已经可以覆盖到工作中大部分场景了,不过我在实践中还是碰到了一些异常的情况,也记录下,说不定对碰到同样问题的同学会有帮助

标记了FLAG_ACTIVITY_NEW_TASK的standard/singleTop的activity成为根activity的情况

异常描述

  • 一个标记了FLAG_ACTIVITY_NEW_TASK的standard/singleTop的Activity实例成为某个task的根activity
  • 要在这个task中再启动相同Activity,使用FLAG_ACTIVITY_NEW_TASK,新的实例不会被创建,task不发生任何变化,根activity中的onNewIntent也不会被回调
  • 不使用FLAG_ACTIVITY_NEW_TASK的话一切正常
  • 如果这个task在后台,那么要启动标记了FLAG_ACTIVITY_NEW_TASK的相同Activity,仅仅会将这个task切到前台,新的实例不会被创建,task中activity不发生任何变化

场景描述

上面说的可能大家有点一头雾水,那么我构建一个还算比较常见的场景

  • 在通知栏上弹出两条通知
  • 设置的Intent都是打开同一个TargetActivity,它的启动模式是standard,因为通知栏和应用的activity没有联系,所以系统在通知点击启动TargetActivity会自动加上FLAG_ACTIVITY_NEW_TASK
  • 如果应用有activity存在的话(TargetActivity不是根activity),点击两条通知会创建两个TargetActivity的实例,这是正常的情况
  • 而当应用的所有activity全部销毁了(弹出通知后按返回键结束activity),点击第一条通知会打开TargetActivity(TargetActivity成为了根activity),点击第二条就没有任何反应

具体的验证代码参见NotificationActivity.kt

建议

对于在Service,Notification等必须加FLAG_ACTIVITY_NEW_TASK才能启动activity的地方需要特别谨慎

  1. 确保要启动的TargetActivity不会成为一个task的根activity
  2. 如果1不能保证,那么应当确保这些地方不会多次启动同一个Activity
  3. 如果1,2都不能保证,那么或许要启动的TargetActivity应该是singleTask的,至少singleTask多次启动时onNewIntent会被调用
  4. 如果singleTask真的适应不了业务场景,那么应当考虑用一个中转的Activity,先启动中转Activity,再在中转Activity中启动真正的TargetActivity

startActivityForResult的异常

先扔两个结论:

  • 使用startActivityForResult启动一个activity的时候,如果不是在当前task中启动的,onActivityResult会立刻回调,resultCode 为RESULT_CANCELED
  • 使用startActivityForResult启动一个activity的时候,如果intent中设置了FLAG_ACTIVITY_NEW_TASK,同样会立刻回调onActivityResult

5.0之前

如果CurrentActivity为singleInstance或者TargetActivity为singleTask/singleInstance,则会在intent中加入FLAG_ACTIVITY_NEW_TASK标志

所以有如下表格

5.0及以上版本的情况

对于android 5.0以上的系统,其实自己也总结了一个规律:如果使用startActivityForResult,Intent中没有主动设置FLAG_ACTIVITY_NEW_TASK,那么在启动TargetActivity时会忽略activity的启动模式,在当前task的栈顶新建一个TargetActivity实例,以保证startActivityForResult可以正常工作。

上面的图看上去很美好,所有的情况都可以覆盖到,但是实际上如果真的用startActivityForResult启动singleTask/singleInstance的activity,会出现了很多预期之外的结果

举个例子

上图从上到下依次全部使用startActivityForResult启动,可以看出明显task中的activity状态不是很符合自己的预期

  • SingleTaskActivity2/SingleInstanceActivity2分别都有两个实例,而正常情况下singleTask/singleInstance启动模式的activity应该系统中只会存在一个实例才对
  • singleInstance的activity本来应该独占task现在也和其他的activty共享了task

事实上,5.0以上系统使用startActivityForResult启动activity可以制造出出很多task中activity不符合预期的场景,基于这些task再用startActivity或者设置FLAG_ACTIVITY_NEW_TASK去启动activity又会衍生出更多的不符合预期的场景,这样的复杂程度老实说自己现在还hold不住,总结不出什么规律,只能具体场景用代码具体实践具体分析

一些关于startActivityForResult的建议

  • 一定不要加FLAG_ACTIVITY_NEW_TASK,这个和startActivityForResult是冲突的
  • 因为不可能不兼容5.0以下的版本,所以CurrentActivity不能是singleInstance TargetActivity不能是singleTask/singleInstance
  • 如果确实要用startActivityForResult启动singleTask/singleInstance的TargetActivity,得同时考虑好5.0以下版本的兼容方式以及5.0以上版本中可能的异常现象

最后

相关的验证代码放在BasicPractice

android基础
Web note ad 1