参考
任务栈和Activity启动模式
Android 面试黑洞——当我按下 Home 键再切回来,会发生什么?
前言
之前在网上检索一些lanchMode的知识,发现一点不言简意赅,有些把实验和结论混合一起的,想法很好,但阅读起来其实很混乱,理解起来也麻烦,没有整体的框架逻辑去看这些的时候,好像学到了啥,其实整体来对比又一团混乱,不体系。
- 建议
本文就只参考了上面两个连接,就完全理解lanchMode的整体框架,建议直接看完本篇以后,再去看这个Android 面试黑洞——当我按下 Home 键再切回来,会发生什么?视频,看看是否自己理解清楚了。理解了这个框架,再去关于Flag的,就都只是些策略问题了。
以下是一些总结和问题,可以看完以后再回看这些,看看自己是否是真的理解了
默认(standard)和singleTop:多用于App内部
singleTask:内部交互和外部交互都会用上,singleInstance:多用于开放给外部App来共享使用
singeTop = standard + 约束
singleInstance = singTask + 约束
在最近任务里看见的Task不一定活着
在最近任务里看不见的Task不一定死了
singleTask流程?
singleInstance怎么能做到独占一个任务栈
预热
在具体开始启动模式相关知识的时候,首先需要搞清楚一下两点
- Task任务栈
- taskAffinity
认清任务栈
首先一个简单的关系图:
- 任务栈结构
任务栈,最直观的表现就是打开最近任务的时候,展示的一个个任务,其实就是一个个task任务栈
安卓系统管理着不同模式下的多个ActivityStack(比如在home launcher界面需要有一个ActivityStack,画中画模式,分屏模式等)。
- 一个ActivityStack可以包含很多个TaskRecord。
- 一个TaskRecord又可以包含很多个ActivityRecord。
- 每一个ActivityRecord都会有一个Activity与之对应,一个Activity可能会有多个ActivityRecord,因为Activity可能被多次实例化。
一系列相关的ActivityRecord组成了一个TaskRecord,TaskRecord是存在于ActivityStack中,ActivityStackSupervisor是用来管理这些ActivityStack的。
launcher也有自己的task,
系统是根据task进行管理的,而不是ActivityStack。
- 前后台task
task又分为前台task和后台task,前台task(也叫当前task)就是栈顶是和用户交互的activity的task,后台task就是非前台task。
Activity可以在Task内部叠成栈,不同task之间也可以叠成栈,不过只针对前台task,前台叠加的多个task,在进入后台会立刻被拆开,这也就是造成"singleTask"模式下回退路径变化的问题。
当前 Activity 启动另一个 Activity 时,该新 Activity 会被推送到堆栈顶部,成为焦点所在。 前一个 Activity 仍保留在堆栈中,但是处于停止状态。Activity 停止时,系统会保持其用户界面的当前状态。 用户按"返回"按钮时,当前 Activity 会从堆栈顶部弹出(Activity 被销毁),而前一个 Activity 恢复执行(恢复其 UI 的前一状态)。
如果用户继续按"返回",堆栈中的相应 Activity 就会弹出,以显示前一个 Activity,直到 所有 Activity 均从堆栈中移除后,任务即不复存在,回到桌面,但此时打开最近任务依然能看到该task的一个“残影”。
taskAffinity
清单文件中Application和Activity标签都可以使用"android:taskAffinity"标记
它代表这个Activity所希望归属的Task,也就是分组,在默认情况下,同一个app中的所有Activity拥有共同的Affinity,即manifest中定义的package。
一个Activity的taskAffinity的优先级:activity设置的 -> Application设置的 -> packageName
taskAffinity与Task、Activity的关系
前面说了,taskAffinity其实就是一个分组标记。
- Activty有"android:taskAffinity"可以更改标记,默认取自Application的标记, Application默认又取自包名。
- 对于Task的affinity标记则取决于它的根部Activity。
多个task可以拥有相同的taskAffinity,但最近列表只会展示最新展示过的那一个task
taskAffinity在两种情况下起作用:
知道taskAffinity是标记作用,就很好理解下面的情况了。
- 当启动Activity的Intent中带有FLAG_ACTIVITY_NEW_TASK标志时。
在默认情况下,目标Activity将与startActivity的调用者处于同一task中。但如果用户特别指定了FLAG_ACTIVITY_NEW_TASK,表明它希望为Activity重新开设一个Task。这时就有两种情况:
- 假如当前已经有一个Task,它的affinity与新Activity是一样的,那么系统会直接用此Task来完成操作,而不是另外创建一个Task;
- 否则系统需要创建一个Task。
- 当Activity中的allowTaskReparenting属性设置为true时。
在这种情况下,Activity具有"动态转移"的能力。举个前面的"短信"例子,在默认情况下,该应用程序中的所有Activity具有相同的affinity。
当另一个程序启动了"短信编辑"时,一开始这个Activity和启动它的Activity处于同样的Task中。但如果"短信编辑"Activity指定了allowTaskReparenting,且后期"短信"程序的Task转为前台,此时"短信编辑"这一Activity会被"挪"到与它更亲近的"短信"Task中。
注意:allowTaskReparenting在安卓9.0和10.0可能有问题
启动模式
在Android中每个界面都是一个Activity,切换界面操作其实是多个不同Activity之间的实例化操作。在Android中Activity的启动模式决定了Activity的启动运行方式。
有两种方式来声明指定启动模式:
- 在清单文件中
- 在java代码中
如果 Activity A 启动 Activity B,则 Activity B 可以在其清单文件中定义它应该如何与当前任务关联(如果可能),并且 Activity A 还可以请求 Activity B 应该如何与当前任务关联。
如果这两个方式均定义 Activity B 应该如何与任务关联,则 Activity A 的请求(Intent 中所定义)优先级要高于 Activity B 的请求(其清单文件中所定义)。
本篇只讲在清单申明的情况
在清单文件中
不管何种方式启动Activity,被启动的Activity通常都要位于ActivityStack的栈顶。
Activity启动方式launchMode,在清单文件中配置。
<activity android:name=".MainActivity"
android:launchMode="xxx" />
您可以分配给 launchMode 属性的启动模式共有四种:
"standard"(默认模式)
- 概述:
和任务栈的taskAffinity没有直接相关,启动一次,就会在当前任务栈直接新创建一个实例
也就是说Activity 可以多次实例化,而每个实例均可属于不同的任务,并且一个任务可以拥有多个该activity实例。
"singleTop"
- 概述:
和"standard"模式类似,只更当前任务栈相关,唯一一点不同是,当要启动的Activity和当前任务栈栈顶的Activity一样时,只调用该实例的 onNewIntent() 方法向其传送 Intent,而不是创建 Activity 的新实例。
例如,假设任务堆栈是 A-B-C-D;D 位于顶部。收到针对 D 类 Activity 的 Intent,
- 如果 D 具有默认的 "standard" 启动模式,则会启动该类的新实例,且堆栈会变成 A-B-C-D-D。
- 如果 D 的启动模式是 "singleTop",则 D 的现有实例会通过 onNewIntent() 接收 Intent,因为它位于堆栈的顶部;而堆栈仍为 A-B-C-D。
- 如果收到针对 B 类 Activity 的 Intent,则会向堆栈添加 B 的新实例,即便其启动模式为 "singleTop" 也是如此。
"singleTask"
- 概述:
这种模式和taskAffinity息息相关,整体逻辑流程如下:
首先根据要启动的Acitivity的taskAffinity找任务栈,分三种情况
- 已存在对应taskAffinity的任务栈,而且就是当前任务栈
- 已存在对应taskAffinity的任务栈,不是当前任务栈
- 不存在对应taskAffinity的任务栈。则创建新的任务栈
注:这里2,3情况,之后任务栈会叠成栈,这种方式只适用于前台task然后在找到对应任务栈以后,就会在栈里面找是否已经有这个Activity了,分成两种情况
a. 已经有了,则把它弹到栈顶(意思是,它如果不在栈顶,就把现在栈顶的一个个弹出,直到它到栈顶为止),并且调用onNewIntent() 方法向其传送 Intent,而不是创建新实例
b. 没有的话,则创建新的实例压栈
下图这种就是 2a组合的情况:
对于这个模式,有个回退路变更问题
- 回退路径,上面提到了任务栈的叠加,因为在切换后台的时候,叠加任务栈会被立刻拆分,所以前台task就只有一个了,回退路径就会发生变化。
这个时候可以考虑属性allowTaskReparenting,希望创建新任务并实例化Activity,当然会受到taskAffinity的影响,即如果已经有一个和要启动的Activity的taskAffinity相同的task,那么就在这个task中创建实例,相反就是创建一个新任务。并且会将这个任务栈压到当前的任务栈
"singleInstance"
- 总结:
这个模式最复杂,该模式下,与 "singleTask" 类似,只是条件更严苛一点。就是此activity独占一个任务栈,而之所以能保证它自己一个独占一个任务栈,是因为多个任务栈可以拥有同一个taskAffinity
一旦该模式的Activity的实例存在于某个栈中,任何应用再激活该Activity时都会重用该栈中的实例,其效果相当于多个应用程序共享一个应用,不管谁激活该Activity都会进入同一个应用中。
singleTask/singleInstance的特别说明
从ActivityStack源码可以看出,singleTask/singleInstance模式会clearTop的效果,
} else if ((launchFlags&Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0
|| r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK
|| r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
从ActivityStack源码可以看出,singleTask/singleInstance模式会自动添加FLAG_ACTIVITY_NEW_TASK
} else if (mLaunchSingleInstance || mLaunchSingleTask) {
// The activity being started is a single instance... it always
// gets launched into its own task.
mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
}
总结
这里面最好理解的就是standard和singleTop两个模式了,而singleTask和singleInstance两个模式必须先要理解 Task和takeInfinity这两个概念,然后搞清楚singleTask流程就行,singleInstance就只是加了一个独占task的条件。
整体结构就这些,看完这个,再去看b站 抛物线的视频,相信会很容易理解了,结构也更加清晰了