前言
上一篇聊到了Activity在启动过程中创建的相关对象,知道在AMS等系统服务中管理的Activity对象实际上是ActivityRecord对象,内部包含了一些列的跟Activity相关的成员属性,在应用开发中,我们经常也会使用到launchMode属性,这个属性就会关联Activity任务栈的概念,当配置不同的启动模式,会影响Activity回退的页面顺序,那么接下来就聊一下什么是任务栈,任务栈和启动模式之间的关系
Task
我们知道当执行startActivity()方法打开一个新的Activiy页面,就会伴随实例化一个ActivityRecord对象指代当前的Activity,在上一篇的ActivityRecord数据结构中,持有了一个Task类型的成员属性,可以看出一个ActivityRecord就对应了一个Task,接下来先从Task的数据结构开始看
class Task extends WindowContainer {
String affinity; //Task的别名,在manifest内部的activity标签可以自定义配置
final int mTaskId; //Task有一个对应的任务ID
final ArrayList mExitingActivities = new ArrayList<>();//Task中使用ArrayList保存内部的Activity
final ActivityStackSupervisor mStackSupervisor;
void addChild(ActivityRecord r) {
addChild(r, Integer.MAX_VALUE /* add on top */);
}
void removeChild(WindowContainer child) {
removeChild(child, "removeChild");
}
}
可以看到Task对象内部通过一个ArrayList持有了一组的ActivityRecord,并且里面提供了一系列的对列表元素操作的方法,所以Task可以理解为执行一组特定的任务的Activity集合,在不配置启动模式的情况下,startActivity后启动的ActivityRecord是默认添加在集合尾部,而当页面退出就会将其移出,这种先入后出的方式跟栈很类似,所以我们经常会把Task叫做Activity的回退栈
通过adb shell dumpsys activity activities | grep packagename命令可以查看当前系统运行的应用进程的任务栈情况
//简单的测试页面,内部一个按钮跳转到一个A页面
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById
从上面可以更加直观的看出Task内部维护了一个ActivityRecord列表,方便管理Activity的页面回退弹出操作
Task管理
通常来说,一个应用进程是由一个一个的Activity交互页面构成的,这些完成一组操作的Activity又构成了一个Task对象,就可以简单的理解一个app就是一个Task,Task也就是用来做后台应用切换的单位,当我们一般通过上滑悬停操作,可以进入操作系统的后台管理页面,这个管理页面其实管理的就是Task,Task可以用来进行任务切换
因为通常情况下一个应用进程就对应一个Task任务栈,所以也就经常理解为后台管理页面管理的是应用进程
Affinity
那么不通常情况下,一个app进程可以维护多个Task任务栈吗?答案当然是可以的,当我们想在一个不同的任务栈去启动一个Activity,可以通过manifest配置文件中给Activity设置一个taskAffinity属性,这个属性就是Task任务栈的别名,默认在不配置的情况下,Task的Affinity值就是应用的包名,设置了别名后,还得在startActivity()启动参数Intent中设置FLAG_ACTIVITY_NEW_TASK属性就可以了,直接看代码
//AndroidManifest.xml
配置了两个页面B和C,都设置了自定义taskAffinity属性
class AActivity:AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_a)
findViewById
当从A->B页面的时候,通过查看任务栈
* Task{5ba46f8 #292 visible=true type=standard mode=fullscreen translucent=true A=10446:test.taskapp U=0 StackId=292 sz=3}
mLastOrientationSource=ActivityRecord{5ebb40f u0 test.taskapp/.BActivity t292}
bounds=[0,0][1080,2400]
* ActivityRecord{5ebb40f u0 test.taskapp/.BActivity t292}
* ActivityRecord{221e302 u0 test.taskapp/.AActivity t292}
* ActivityRecord{7534e5b u0 test.taskapp/.MainActivity t292}
可以看到,B页面的ActivityRecord跟前面的页面在同一个Task内部
* Task{b3bb3f4 #293 visible=true type=standard mode=fullscreen translucent=true A=10446:.CTask U=0 StackId=293 sz=1}
mLastOrientationSource=ActivityRecord{c8aa1c7 u0 test.taskapp/.CActivity t293}
bounds=[0,0][1080,2400]
* ActivityRecord{c8aa1c7 u0 test.taskapp/.CActivity t293}
* Task{5ba46f8 #292 visible=true type=standard mode=fullscreen translucent=true A=10446:test.taskapp U=0 StackId=292 sz=2}
mLastOrientationSource=ActivityRecord{221e302 u0 test.taskapp/.AActivity t292}
bounds=[0,0][1080,2400]
* ActivityRecord{221e302 u0 test.taskapp/.AActivity t292}
* ActivityRecord{7534e5b u0 test.taskapp/.MainActivity t292}
当跳转参数设置了FLAG_ACTIVITY_NEW_TASK标志位后,新生成的CActivity就在一个新的Task任务栈中,同时观察后台的Task列表可以看出,我们的应用进程就有两个不同的窗体
跨进程Activity
同一个应用进程可以管理多个不同的Task任务栈,那么不同的进程的Activity可以在用一个任务栈么,接着从代码中取看一下
//新增一个D页面,并且配置其process属性
//继续从A页面跳转到D页面后查看任务栈
* Task{5ba46f8 #292 visible=true type=standard mode=fullscreen translucent=true A=10446:test.taskapp U=0 StackId=292 sz=3}
mLastOrientationSource=ActivityRecord{e6a638f u0 test.taskapp/.DActivity t399}
bounds=[0,0][1080,2400]
* ActivityRecord{e6a638f u0 test.taskapp/.DActivity t292}
* ActivityRecord{221e302 u0 test.taskapp/.AActivity t292}
* ActivityRecord{7534e5b u0 test.taskapp/.MainActivity t292}
可以看到不同的应用进程的Activity也可以使用同一个任务栈进行管理,ActivityRecord对象就是用于AMS去管理Activity,而AMS本身就是跨进程通信的系统服务,所以Task能关联不同进程的ActivityRecord也就能理解
小结
通过一系列的分析,应该对Task有了一个更全面的认知,在平时开发中可能很少使用taskAffinity属性去新开一个Task管理页面栈,但是如果对回退栈有更精细的操作管理,可以使用这个方式去做不同的任务栈的隔离,而且Task也可以管理跨进程的页面
launchMode
跟Task相关的概念还有一个启动模式,这个在平时应用开发中还是比较常用,可以直接在AndroidManifest.xml文件中进行配置
从图中可以看到,启动模式可以配置5种,不同的启动模式会影响Task内部的ActivityRecord列表顺序
standard:这个是Android的默认启动模式,每打开一个Activity页面就会在当前Task内部创建一个对应的ActiviyRecord实例,同一个Activity可以存在多个实例
singleTask:从英文意思看就能知道是在Task中只能有一个对应类型的Activity实例,如果当前类型的Activity已经在Task栈内已经有对应的ActivityRecord,就会将其上面的Activity出栈清理掉,如果没有就正常的加入Task内,需要注意的是,如果配置了taskAffnity,在不使用FLAG_ACTIVITY_NEW_TASK标志位的情况下,如果没有对应taskAffnity命名的任务栈,也会新实例化一个对应taskAffnity的任务栈,然后将对应的ActivityRecord加入进去
singleInstance:不同于singleTask,设置了这个模式,整个系统中只能存在这个类型的Activity实例,在不同的Task中也不能重复存在
singleInstancePerTask:这个是Android12新增的启动模式,类似于singleTask,也是在一个Task内部保持唯一类型的Activity对象,不同的在于,singeTask需要配置taskAffnity属性,就会在一个不同的任务栈中启动对应的Activity,singleInstancePerTask不需要配置taskAffnity,直接调用start就会默认新开一个Task去管理,但是这个默认新建Task的行为只执行一次,当后续再次启动对应的Activity,如果不额外配置,就会在新的Task任务栈内执行singleTask逻辑,直接将对应的Activity弹到栈顶,如果配置了Intent.FLAG_ACTIVITY_MULTIPLE_TASK或Intent.FLAG_ACTIVITY_NEW_DOCUMENT属性,就会每次都创建一个新的Task去管理调用栈
singleTop:从英文意思能知道是栈顶唯一,那就是如果当需要启动的Activity类型已经在栈顶,那么当再次启动就不会去创建新的ActivityRecord对象,但是如果对应的类型没有在栈顶,那么调用栈就会出现两个ActivityRecord都是对应的Activity类型
启动模式举例写demo太麻烦了,这个平时开发中都有用过,举一些简单的例子说明一下
举个:比如一个新闻列表页A,进入新闻详情页B,底部又有推荐新闻可以点击跳转跳转新闻详情页,如果不配置启动模式,以standard的行为模式执行startActivity(),整个任务栈内可能就成了A->B->B->B->B...这样想回到列表页A继续操作,就得点击多次返回按钮,这个时候可以给B页面配置singleTop模式,当B在栈顶去再次打开B页面的时候的,就不产生重复的实例,最后的调用栈就是A->B,这样只需要一次返回就能到达A页面
再举个:一个应用从首页A跳转到登录页面B,然后继续去跳转注册页面C,当注册完成后,直接返回A页面,如果使用默认的跳转方式,任务栈就会成了A->B->C->A,如果这个时候双击想要退出应用,就会发现直接跳转到了C页面,这个时候给A配置singleTask,那么当从C跳转到A后,A弹到栈顶,B和C页面就会被弹出清理掉,最终Task内部就只有A一个实例
ActivityStack
从其他的博文中可以了解到,在之前的版本中,ActivityStack是用来管理不同的Task做前后台切换的,而我目前看的Android11的源码内,ActivityStack就是一个继承自Task的类,这样命名更加符合我们理解的Activity启动过程中先入后出的栈的概念
/**
* State and management of a single stack of activities.
*/
class ActivityStack extends Task {}
总结
通过上面的一些列分析,我们对ActivityRecord,Task还有ActivityStack有了认知,Task和ActivityStack是继承关系,都是用来管理ActivityRecord的,Task内部维护了ActivityRecord列表,并且通过不同的launchMode对这个列表进程添加/移除操作
有了这些概念基础,下一篇就继续回到startActiviy()的源码流程中,看看再启动的时候去怎么判断创建Task,判断launchMode标志,添加ActivityRecord实例的