細説Android的Launch Mode

語言: CN / TW / HK

theme: fancy

一. 多任務和Task、啟動模式

Android 手機在早期,下方通常會內置三個實體的觸摸按鍵,分別是:桌面菜單返回。大概在Android 5.0 之後,Android開始流行系統內置虛擬按鍵,其中的菜單被替換成了多任務,一旦我們按下多任務按鍵,一個個的任務快照就以流的形式展現在屏幕之上。

隨着Google對多任務更好地支持,越來越多的廠商將正面的實體按鍵,替換成了虛擬按鍵,也將菜單按鍵刪除,替換成了多任務視圖按鈕,顯然多任務在之後的Android版本中是非常重要的一個概念。

甚至由於虛擬按鍵的出現,一些特定型號的手機在下方可能會形成奇怪的五層下巴: WX20220603-011757@2x.png

我們將這一個個的任務叫做Task,多任務視圖中,會顯示各個Task頂層Activity的快照(之所以是快照,是因為Activity不一定是存活的,有可能它只是一張圖片。)Task中,以回退棧的形式堆疊Activity。

image.png

每個Task,對應一個名稱,如果我們不去設置,那麼就是我們Application的應用包名,默認情況下,start一個新的Activity就會被裝入該Task當中,然後在這個Task中進行堆疊,新打開的Activity在上方,用户可以通過按下返回鍵回退到上一個Activity當中。

每個Activity,都有一個TaskAffinity屬性,標誌了它想去的Task是哪一個,如果你不填寫,那麼通常是默認的目標棧,但是要注意的是,TaskAffinity需要和LaunchMode搭配在一起使用: - 如果不設置LaunchMode(即採用默認的Standard),那麼你從ActivityA中啟動一個設置了TaskAffinity的ActivityB,你會發現它不生效,它仍然在當前包名的ActivityA的對應的任務棧當中。 - 如果你將TaskAffinity配置和LaunchMode = SingleTask一起使用,在你打開了ActivityB時,你按下多任務按鈕,你會發現一個App出現了兩個Task,即兩個回退棧,這兩個回退棧的分別對應ActivityA和ActivityB的對應的Task的回退棧:

image (1).png

它們擁有相同的項目名稱,因為只是名為router的app模塊下的兩個不同TaskAffinity的Activity,不過這個TaskAffinity並不顯示在多任務視圖當中。

二. 四種啟動模式詳解

所以,四種啟動模式,對應的具體的啟動情況如下:

1. Standard

在當前的Task的回退棧中,啟動一個Activity實例,放在棧頂。

在這種模式下,使用TaskAffinity是無效的。即使填寫了TaskAffinity,最終也會被創建在執行啟動命令的Activity對應的Task棧的棧頂,而不是TaskAffinity對應的Task的棧頂。

2. SingleTask

在TaskAffinity指定的退回棧中嘗試啟動一個Activity實例: - 如果指定的回退棧中,含有該Activity相同類型的實例,那麼就回調onNewIntent()方法,告知原先已經存在的實例X,然後回調onResume()方法,並將原先實例上方的實例全部移除出回退棧,這樣一來,原先已經存在的實例X就會出現在棧頂。 - 如果指定的回退棧中,不包含Activity相同類型的實例,那麼就在棧頂創建,走正常的生命週期回調:onCreate()->onStart()->onResume() - 如果TaskAffinity對應的Task都不存在的情況下,會先去創建目標Task,再走創建Activity實例的流程,最後壓入棧頂。 - 如果沒有指定TaskAffinity,那麼就指定為當前調用啟動操作的Activity的Task,將Activity壓入該Task回退棧的棧頂。

注意: 1. 創建在當前Task和其它Task的Activity跳轉動畫是不相同的。 2. SingleTask + TaskAffinity實際上也是一種全局的單例,因為它的創建結果最終都會將Activity創建在指定的TaskAffinity的Task下。即使不填寫TaskAffinity,也會只在當前Activity對應的Task下創建或者複用唯一的一個實例。

3. SingleTop

在TaskAffinity指定的退回棧中嘗試在啟動一個Activity實例。和SingleTask類似,但是複用條件稍有不同。僅在目標Task的回退棧的頂部含有相同類型的Activity時,觸發複用,回調onNewIntent和onResume。

4.SingleInstance

在TaskAffinity指定的退回棧中,創建一個Activity實例,但是,一個Task中僅允許一個Activity存在,如果兩個Activity對應的TaskAffinity是相同的,例如從A中以SingleInstance啟動B,B中以SingleInstance啟動C,BC的TaskAffinity是相同的。

這種情況下,如果我們在C中,呼出多任務視圖菜單,我們會發現,此時棧中只有C和A對應的Task,B對應的Task並不存在,你可能會認為B和C在同一個棧中,B在C之下。但是如果你以另外一種方式: 在C中,呼出多任務菜單,回到C後,然後點擊返回,你認為應該回到B,但是你會發現,直接退到桌面了。 或者你在C中,直接按回車,你會發現從C->B和從B->A的跳轉過場動畫,都是Task間的轉場動畫,而不是Task內部的跳轉動畫。

這涉及到另外我們就要講另外一個問題,Task間跳轉時,Task間的堆疊問題(Task疊在另一個Task上面),而打開多任務列表或者按下Home鍵會導致堆疊被破壞。

B和C即使有同一個TaskAffinity命名,並且根據我們説的一個Task中僅允許一個Activity存在,C打開時,B應該被關閉,從多任務上來看,似乎也是這樣的,因為只有C和A的Task在多任務視圖中,但是我們確實又可以從C的Task返回到B的Task,在任務棧中我們又看不到B的身影,這可以下一個結論:多任務視圖中的不可見的Task不一定不存在,如果發生SingleInstance + TaskAffinity衝突的情況,例如:

Activity A和Activity B都是SingleInstance的,並且又都是設置了TaskAffinity為 com.example.newTask。如果既要保證AB都在同一個Task中,又要保證該Task只能有一個>Activity,那麼就會導致衝突。

經過測試,當衝突發生時,Android會為兩個Activity都創建一個Task,但是同一時間,只有一個能>在多任務視圖中被看見,但是如果順序啟動:A、B,是可以在B中回退到A中的。並且回退的動畫是Task 間切換的動畫。

那麼Task中可見的Activity一定在運行嗎?答案也是否定的,如果我們在ActivityA按下返回鍵,退回到桌面,我們此時打開多任務,我們會發現,ActivityA的Task的快照仍然保留在多任務視圖之上,但是它此時已經“死了”,我們點擊它,實際上是創建了一個新的ActivityA。所以,多任務視圖中中的不可見的Task不一定不存在,多任務視圖中可見的Activity也不一定就是Running狀態的

三. Task間堆疊與Task Reparenting

3.1 Task間堆疊

考慮如下的兩個任務棧間切換場景:

image (2).png 我們需要從ActivityC中啟動Activity E,此時ActivityE的啟動模式被設置成了:SingleTask,TaskAffinity是Task2。ActivityE被啟動之後,在屏幕上顯示出來了。

基於這個狀態,接下來有幾個問題:

  • 問題1:如果此時按多次返回鍵,會發生什麼?
  • 問題2:如果此時先按Home回到桌面,再從多任務列表打開Task1,顯示的是ActivityC還是ActivityE?
  • 問題3:如果此時先按Home回到桌面,再從多任務列表打開Task2,顯示的是ActivityE,如果此時不不斷地按返回鍵,會發什麼?
  • 問題4:如果此時先打開多任務列表,再按返回,返回Task2,此時顯示的是ActivityE。如果此時不不斷地按返回鍵,會發什麼?

對於問題1,答案是:Activity E/D/C/B/A同時出棧,A出棧之後回到桌面。 對於問題2,答案是:顯示的是ActivityC 對於問題3、4,答案是:Activity E/D出棧,D出棧之後,回到桌面,而不是Activity C。

問題1的原因是因為,Task2的任務被打開之後,整個Task2成為了優先顯示的Task,被堆在屏幕之上,一旦Task2的Activity退完了,Task1可以無縫地銜接上:

image (3).png 就好像Task2被堆在了Task1之上,這樣的堆疊,保證了用户交互的連貫性。

這樣的Task間的堆疊跳轉特性適用於用户跳轉某個頁面之後,按返回鍵不想馬上跳回原Activity的一種情況。

但是,這種堆疊僅限於Task跳轉剛剛發生的情況,一旦用户進行了: - Home鍵返回桌面 - 切出多任務視圖

這類的操作,將頂端的Task變為後台Task之後,那麼這種「堆疊」就會立刻失效,並且不再恢復,所以在問題3、4中,最後返回的是桌面,而不是Task1的ActivityC,這種堆疊被破壞了,Task1和Task2又重新回到平級的狀態了。

但是在最新的API32版本中,切到Android 自帶的多任務視圖並不會導致Task2重新被移動到Task1平級的位置,這意味着,你從多任務切回來之後,在ActivityE按下返回,仍然會回退到到Task1的ActivityC之上。

如果有一些場景,你希望打開ActivityE之後不退到D,直接退到C那應該怎麼做呢?其實不設置SingleTask就好了,默認的就是這種情況,比如外賣平台支付訂單之後,付款軟件的的付款結果頁的一個實例就被留在了外賣軟件的Task中。

3.2 Task Reparenting/Task重定父級

通常來説,一個Activity被裝進一個Task的棧之後,就不會去移動了,但是我們可以藉助Task Reparenting來做父級的重新指定。

例如上述的例子中,我們隊ActivityE設置:allowTaskReparenting。我們從Task1中的ActivityC啟動ActivityE時,ActivityE會啟動在Task1中。

一旦我們切到桌面,再重新從桌面圖標重新打開Task1時,我們會發現ActivityE從Task1中消失了,而從桌面打開Task2對應的App圖標時,會發現,ActivityE重新回到了自己的Task2中,例如我們按照如下定義: ```

```

如果我們在默認的,和包名相同的com.rEd.router下創建該MainActivityE,那麼此時的MainActivity會在當前的com.rEd.router下創建該MainActivityE實例,然後我退到桌面,再重新在桌面的App圖標打開App,我會發現,啟動的App的當前的Activity變成了ActivityC,而不是ActivityE:

未命名文件 (7).png

而且,多任務視圖中的Task2中的快照是空白的,我們點開Task2,發現ActivityE回到了它taskAffinity指定的Task中:

image (4).png

另外,如果同時設置了allowTaskReparenting=true和LaunchMode,那麼LaunchMode會優先生效,Activity會直接創建在其他的Task中。

參考來源

1.Android 面試黑洞——當我按下 Home 鍵再切回來,會發生什麼? - 掘金 (juejin.cn)