從源碼中理解Activity組件(2)-頁面棧Task

前言

上一篇聊到了Activity在啟動過程中創(chuàng)建的相關(guān)對象,知道在AMS等系統(tǒng)服務(wù)中管理的Activity對象實際上是ActivityRecord對象,內(nèi)部包含了一些列的跟Activity相關(guān)的成員屬性,在應(yīng)用開發(fā)中,我們經(jīng)常也會使用到launchMode屬性,這個屬性就會關(guān)聯(lián)Activity任務(wù)棧的概念,當(dāng)配置不同的啟動模式,會影響Activity回退的頁面順序,那么接下來就聊一下什么是任務(wù)棧,任務(wù)棧和啟動模式之間的關(guān)系

Task

我們知道當(dāng)執(zhí)行startActivity()方法打開一個新的Activiy頁面,就會伴隨實例化一個ActivityRecord對象指代當(dāng)前的Activity,在上一篇的ActivityRecord數(shù)據(jù)結(jié)構(gòu)中,持有了一個Task類型的成員屬性,可以看出一個ActivityRecord就對應(yīng)了一個Task,接下來先從Task的數(shù)據(jù)結(jié)構(gòu)開始看

class Task extends WindowContainer<WindowContainer> {
  String affinity; //Task的別名,在manifest內(nèi)部的activity標(biāo)簽可以自定義配置
  final int mTaskId; //Task有一個對應(yīng)的任務(wù)ID
  final ArrayList<ActivityRecord> mExitingActivities = new ArrayList<>();//Task中使用ArrayList保存內(nèi)部的Activity
    final ActivityStackSupervisor mStackSupervisor;
  
  void addChild(ActivityRecord r) {
    addChild(r, Integer.MAX_VALUE /* add on top */);
  }
  
  void removeChild(WindowContainer child) {
    removeChild(child, "removeChild");
  }
}

可以看到Task對象內(nèi)部通過一個ArrayList持有了一組的ActivityRecord,并且里面提供了一系列的對列表元素操作的方法,所以Task可以理解為執(zhí)行一組特定的任務(wù)的Activity集合,在不配置啟動模式的情況下,startActivity后啟動的ActivityRecord是默認(rèn)添加在集合尾部,而當(dāng)頁面退出就會將其移出,這種先入后出的方式跟棧很類似,所以我們經(jīng)常會把Task叫做Activity的回退棧

通過adb shell dumpsys activity activities | grep packagename命令可以查看當(dāng)前系統(tǒng)運行的應(yīng)用進(jìn)程的任務(wù)棧情況

//簡單的測試頁面,內(nèi)部一個按鈕跳轉(zhuǎn)到一個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
//執(zhí)行跳轉(zhuǎn)前
      * 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}

//執(zhí)行跳轉(zhuǎn)后
      * 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內(nèi)部維護(hù)了一個ActivityRecord列表,方便管理Activity的頁面回退彈出操作

Task管理

通常來說,一個應(yīng)用進(jìn)程是由一個一個的Activity交互頁面構(gòu)成的,這些完成一組操作的Activity又構(gòu)成了一個Task對象,就可以簡單的理解一個app就是一個Task,Task也就是用來做后臺應(yīng)用切換的單位,當(dāng)我們一般通過上滑懸停操作,可以進(jìn)入操作系統(tǒng)的后臺管理頁面,這個管理頁面其實管理的就是Task,Task可以用來進(jìn)行任務(wù)切換

后臺任務(wù)Task管理頁面.jpeg

因為通常情況下一個應(yīng)用進(jìn)程就對應(yīng)一個Task任務(wù)棧,所以也就經(jīng)常理解為后臺管理頁面管理的是應(yīng)用進(jìn)程

Affinity

那么不通常情況下,一個app進(jìn)程可以維護(hù)多個Task任務(wù)棧嗎?答案當(dāng)然是可以的,當(dāng)我們想在一個不同的任務(wù)棧去啟動一個Activity,可以通過manifest配置文件中給Activity設(shè)置一個taskAffinity屬性,這個屬性就是Task任務(wù)棧的別名,默認(rèn)在不配置的情況下,Task的Affinity值就是應(yīng)用的包名,設(shè)置了別名后,還得在startActivity()啟動參數(shù)Intent中設(shè)置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,都設(shè)置了自定義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)
            })
        }
    }
}

當(dāng)從A->B頁面的時候,通過查看任務(wù)棧

      * 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內(nèi)部

配置了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}

當(dāng)跳轉(zhuǎn)參數(shù)設(shè)置了FLAG_ACTIVITY_NEW_TASK標(biāo)志位后,新生成的CActivity就在一個新的Task任務(wù)棧中,同時觀察后臺的Task列表可以看出,我們的應(yīng)用進(jìn)程就有兩個不同的窗體

同一個應(yīng)用進(jìn)程可配置不同的Task任務(wù).jpeg
跨進(jìn)程Activity

同一個應(yīng)用進(jìn)程可以管理多個不同的Task任務(wù)棧,那么不同的進(jìn)程的Activity可以在用一個任務(wù)棧么,接著從代碼中取看一下

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

//繼續(xù)從A頁面跳轉(zhuǎn)到D頁面后查看任務(wù)棧
       * 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}     

可以看到不同的應(yīng)用進(jìn)程的Activity也可以使用同一個任務(wù)棧進(jìn)行管理,ActivityRecord對象就是用于AMS去管理Activity,而AMS本身就是跨進(jìn)程通信的系統(tǒng)服務(wù),所以Task能關(guān)聯(lián)不同進(jìn)程的ActivityRecord也就能理解

小結(jié)

通過一系列的分析,應(yīng)該對Task有了一個更全面的認(rèn)知,在平時開發(fā)中可能很少使用taskAffinity屬性去新開一個Task管理頁面棧,但是如果對回退棧有更精細(xì)的操作管理,可以使用這個方式去做不同的任務(wù)棧的隔離,而且Task也可以管理跨進(jìn)程的頁面

launchMode

跟Task相關(guān)的概念還有一個啟動模式,這個在平時應(yīng)用開發(fā)中還是比較常用,可以直接在AndroidManifest.xml文件中進(jìn)行配置

launchMode的可選類型.png

從圖中可以看到,啟動模式可以配置5種,不同的啟動模式會影響Task內(nèi)部的ActivityRecord列表順序

  • standard:這個是Android的默認(rèn)啟動模式,每打開一個Activity頁面就會在當(dāng)前Task內(nèi)部創(chuàng)建一個對應(yīng)的ActiviyRecord實例,同一個Activity可以存在多個實例

  • singleTask:從英文意思看就能知道是在Task中只能有一個對應(yīng)類型的Activity實例,如果當(dāng)前類型的Activity已經(jīng)在Task棧內(nèi)已經(jīng)有對應(yīng)的ActivityRecord,就會將其上面的Activity出棧清理掉,如果沒有就正常的加入Task內(nèi),需要注意的是,如果配置了taskAffnity,在不使用FLAG_ACTIVITY_NEW_TASK標(biāo)志位的情況下,如果沒有對應(yīng)taskAffnity命名的任務(wù)棧,也會新實例化一個對應(yīng)taskAffnity的任務(wù)棧,然后將對應(yīng)的ActivityRecord加入進(jìn)去

  • singleInstance:不同于singleTask,設(shè)置了這個模式,整個系統(tǒng)中只能存在這個類型的Activity實例,在不同的Task中也不能重復(fù)存在

  • singleInstancePerTask:這個是Android12新增的啟動模式,類似于singleTask,也是在一個Task內(nèi)部保持唯一類型的Activity對象,不同的在于,singeTask需要配置taskAffnity屬性,就會在一個不同的任務(wù)棧中啟動對應(yīng)的Activity,singleInstancePerTask不需要配置taskAffnity,直接調(diào)用start就會默認(rèn)新開一個Task去管理,但是這個默認(rèn)新建Task的行為只執(zhí)行一次,當(dāng)后續(xù)再次啟動對應(yīng)的Activity,如果不額外配置,就會在新的Task任務(wù)棧內(nèi)執(zhí)行singleTask邏輯,直接將對應(yīng)的Activity彈到棧頂,如果配置了Intent.FLAG_ACTIVITY_MULTIPLE_TASK或Intent.FLAG_ACTIVITY_NEW_DOCUMENT屬性,就會每次都創(chuàng)建一個新的Task去管理調(diào)用棧

  • singleTop:從英文意思能知道是棧頂唯一,那就是如果當(dāng)需要啟動的Activity類型已經(jīng)在棧頂,那么當(dāng)再次啟動就不會去創(chuàng)建新的ActivityRecord對象,但是如果對應(yīng)的類型沒有在棧頂,那么調(diào)用棧就會出現(xiàn)兩個ActivityRecord都是對應(yīng)的Activity類型

啟動模式舉例寫demo太麻煩了,這個平時開發(fā)中都有用過,舉一些簡單的例子說明一下

舉個??:比如一個新聞列表頁A,進(jìn)入新聞詳情頁B,底部又有推薦新聞可以點擊跳轉(zhuǎn)跳轉(zhuǎn)新聞詳情頁,如果不配置啟動模式,以standard的行為模式執(zhí)行startActivity(),整個任務(wù)棧內(nèi)可能就成了A->B->B->B->B...這樣想回到列表頁A繼續(xù)操作,就得點擊多次返回按鈕,這個時候可以給B頁面配置singleTop模式,當(dāng)B在棧頂去再次打開B頁面的時候的,就不產(chǎn)生重復(fù)的實例,最后的調(diào)用棧就是A->B,這樣只需要一次返回就能到達(dá)A頁面

再舉個??:一個應(yīng)用從首頁A跳轉(zhuǎn)到登錄頁面B,然后繼續(xù)去跳轉(zhuǎn)注冊頁面C,當(dāng)注冊完成后,直接返回A頁面,如果使用默認(rèn)的跳轉(zhuǎn)方式,任務(wù)棧就會成了A->B->C->A,如果這個時候雙擊想要退出應(yīng)用,就會發(fā)現(xiàn)直接跳轉(zhuǎn)到了C頁面,這個時候給A配置singleTask,那么當(dāng)從C跳轉(zhuǎn)到A后,A彈到棧頂,B和C頁面就會被彈出清理掉,最終Task內(nèi)部就只有A一個實例

ActivityStack

從其他的博文中可以了解到,在之前的版本中,ActivityStack是用來管理不同的Task做前后臺切換的,而我目前看的Android11的源碼內(nèi),ActivityStack就是一個繼承自Task的類,這樣命名更加符合我們理解的Activity啟動過程中先入后出的棧的概念

/**
 * State and management of a single stack of activities.
 */
class ActivityStack extends Task {}
總結(jié)

通過上面的一些列分析,我們對ActivityRecord,Task還有ActivityStack有了認(rèn)知,Task和ActivityStack是繼承關(guān)系,都是用來管理ActivityRecord的,Task內(nèi)部維護(hù)了ActivityRecord列表,并且通過不同的launchMode對這個列表進(jìn)程添加/移除操作

有了這些概念基礎(chǔ),下一篇就繼續(xù)回到startActiviy()的源碼流程中,看看再啟動的時候去怎么判斷創(chuàng)建Task,判斷l(xiāng)aunchMode標(biāo)志,添加ActivityRecord實例的

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容