从源码中理解Activity组件(2)-页面栈Task

前言

上一篇聊到了Activity在启动过程中创建的相关对象,知道在AMS等系统服务中管理的Activity对象实际上是ActivityRecord对象,内部包含了一些列的跟Activity相关的成员属性,在应用开发中,我们经常也会使用到launchMode属性,这个属性就会关联Activity任务栈的概念,当配置不同的启动模式,会影响Activity回退的页面顺序,那么接下来就聊一下什么是任务栈,任务栈和启动模式之间的关系

Task

我们知道当执行startActivity()方法打开一个新的Activiy页面,就会伴随实例化一个ActivityRecord对象指代当前的Activity,在上一篇的ActivityRecord数据结构中,持有了一个Task类型的成员属性,可以看出一个ActivityRecord就对应了一个Task,接下来先从Task的数据结构开始看

class Task extends WindowContainer<WindowContainer> {
  String affinity; //Task的别名,在manifest内部的activity标签可以自定义配置
  final int mTaskId; //Task有一个对应的任务ID
  final ArrayList<ActivityRecord> 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<Button>(R.id.button).setOnClickListener {
            startActivity(Intent(this, AActivity::class.java))
        }
    }
}

//>>> adb shell dumpsys activity activities | grep test.taskapp
//执行跳转前
      * Task{5ba46f8 #292 visible=true type=standard mode=fullscreen translucent=true A=10446:test.taskapp U=0 StackId=292 sz=1}
        mLastOrientationSource=ActivityRecord{7534e5b u0 test.taskapp/.MainActivity t292}
        bounds=[0,0][1080,2400]
        * ActivityRecord{7534e5b u0 test.taskapp/.MainActivity t292}

//执行跳转后
      * 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}

从上面可以更加直观的看出Task内部维护了一个ActivityRecord列表,方便管理Activity的页面回退弹出操作

Task管理

通常来说,一个应用进程是由一个一个的Activity交互页面构成的,这些完成一组操作的Activity又构成了一个Task对象,就可以简单的理解一个app就是一个Task,Task也就是用来做后台应用切换的单位,当我们一般通过上滑悬停操作,可以进入操作系统的后台管理页面,这个管理页面其实管理的就是Task,Task可以用来进行任务切换

后台任务Task管理页面.jpeg

因为通常情况下一个应用进程就对应一个Task任务栈,所以也就经常理解为后台管理页面管理的是应用进程

Affinity

那么不通常情况下,一个app进程可以维护多个Task任务栈吗?答案当然是可以的,当我们想在一个不同的任务栈去启动一个Activity,可以通过manifest配置文件中给Activity设置一个taskAffinity属性,这个属性就是Task任务栈的别名,默认在不配置的情况下,Task的Affinity值就是应用的包名,设置了别名后,还得在startActivity()启动参数Intent中设置FLAG_ACTIVITY_NEW_TASK属性就可以了,直接看代码

//AndroidManifest.xml
<activity android:name=".AActivity" />

<activity
  android:name=".BActivity"
  android:taskAffinity=".BTask" />

<activity
   android:name=".CActivity"
   android:taskAffinity=".CTask" />

配置了两个页面B和C,都设置了自定义taskAffinity属性

class AActivity:AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_a)

        findViewById<Button>(R.id.button).setOnClickListener {
            //B页面没有添加FLAG_ACTIVITY_NEW_TASK
            startActivity(Intent(this, BActivity::class.java))
        }
                      //A页面添加FLAG_ACTIVITY_NEW_TASK
        findViewById<Button>(R.id.button2).setOnClickListener {
            startActivity(Intent(this, CActivity::class.java).apply {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            })
        }
    }
}

当从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内部

配置了taskAffinity没使用flag.jpeg
      * 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列表可以看出,我们的应用进程就有两个不同的窗体

同一个应用进程可配置不同的Task任务.jpeg
跨进程Activity

同一个应用进程可以管理多个不同的Task任务栈,那么不同的进程的Activity可以在用一个任务栈么,接着从代码中取看一下

//新增一个D页面,并且配置其process属性
<activity
    android:name=".DActivity"
    android:process=":TaskDProcess" />

//继续从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文件中进行配置

launchMode的可选类型.png

从图中可以看到,启动模式可以配置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实例的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容