Activity啓動模式一

衆所周知,Activity有4種啓動模式,分別是:Standard、SingleTop、SingleTask和SingleInstance,它們控制了被啓動Activity的啓動行爲。本文將經過具體案例,詳細分析這幾種模式的差別和使用場景,方便往後查閱。java

在展開具體分析以前,咱們首先要了解下兩個基礎知識:Activity任務棧和android:taskAffinity屬性。android

基礎知識

Activity任務棧(Task)

Activity任務棧(Task)是一個標準的棧結構,具備「First In Last Out」的特性,用於在ActivityManagerService側管理全部的Activity(AMS經過TaskRecord標識一個任務棧,經過ActivityRecord標識一個Activity)。git

每當咱們打開一個Activity時,就會有一個Activity組件被添加到任務棧,每當咱們經過「back」鍵退出一個Activity時,就會有一個Activity組件從任務棧出棧。任意時刻,只有位於棧頂的Activity才能夠跟用戶進行交互。github

同一時刻,Android系統能夠有多個任務棧;每一個任務棧可能有一個或多個Activity,這些Activity可能來自於同一個應用程序,也可能來自於多個應用程序。另外,同一個Activity可能只有一個實例,也可能有多個實例,並且這些實例既可能位於同一個任務棧,也可能位於不一樣的任務棧。而這些行爲均可以經過Activity啓動模式進行控制。shell

在Android系統的多個任務棧中,只有一個處於前臺,即前臺任務棧,其它的都位於後臺,即後臺任務棧。後臺任務棧中的Activity處於暫停狀態,用戶能夠經過喚起後臺任務棧中的任意Activity,將後臺任務棧切換到前臺。bash

android:taskAffinity屬性

android:taskAffinity是Activity的一個屬性,表示該Activity指望的任務棧的名稱。默認狀況下,一個應用程序中全部Activity的taskAffinity都是相同的,即應用程序的包名。固然,咱們能夠在配置文件中爲每一個Activity指定不一樣的taskAffinity(只有和已有包名不一樣,纔有意義)。通常狀況下,該屬性主要和SingleTask啓動模式或者android:allowTaskReparenting屬性結合使用(下面會詳細介紹),在其餘狀況下沒有意義。ide

四種啓動模式

上面簡要介紹了Activity任務棧和android:taskAffinity屬性。有了這些基礎以後,咱們就能夠詳細介紹Activity的四種啓動模式了。函數

通常狀況下,咱們在AndroidManifest配置文件中,爲Activity指定啓動模式和taskAffinity,以下所示:ui

<activity 
android:name="leon.com.activitylaunchmode.FirstActivity" 
android:launchMode="singleTop"
android:taskAffinity="leon.com.activitylaunchmode1"/>
複製代碼

除此以外,咱們也能夠經過設置Intent的某些標誌位來達到相同的效果,關於這些和Activity啓動模式相關的標誌位,咱們會在下篇文章進行介紹。this

Standard

標準模式,也是系統的默認模式。該模式下,每次啓動Activity,都會建立一個新實例,而且將其加入到啓動該Activity的那個Activity所在的任務棧中,因此目標Activity的多個實例能夠位於不一樣的任務棧。例如:ActivityA啓動了標準模式的ActivityB,那麼ActivityB就會在ActivityA所在的任務棧中。

關於標準模式的Activity,有一個很經典的異常: 當咱們經過非Activity的Context(例如:Service)啓動標準模式的Activity時,就會有如下異常:

Caused by: Android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want? 複製代碼

這是由於標準模式的Activity會進入啓動它的Activity的任務棧中,可是非Activity的Context又沒有任務棧,因此就出錯了。解決方案在異常信息中已經給出了:在Intent中添加FLAG_ACTIVITY_NEW_TASK標誌位,這樣就會爲目標Activity建立新的任務棧。

OK,下面咱們經過示例驗證下該啓動模式,具體步驟以下所示:

  1. 建立MainActivity,而且在MainActivity的onCreate方法中,打印出當前Activity所在的任務棧ID。
  2. 設置MainActivity的啓動模式爲Standard。
  3. 而後經過MainActivity不斷啓動MainActivity自己。

通過上述步驟,最終的任務棧以下所示(adb shell dumpsys activity activities):

Standard模式

經過MainActivity打印出的日誌以下所示:

Standard模式日誌

經過上面的任務棧和日誌可知:每次啓動MainActivity,都建立了新的實例,且每一個實例都在一個任務棧中(任務棧ID都是8191),最終一個任務棧中累積了多個MainActivity的實例。這樣每次回退,都會看到相同的MainActivity。

SingleTop

棧頂複用模式。該模式下,若目標Activity的實例已經存在,可是沒有位於棧頂,那麼仍然會建立新的實例,並添加到任務棧;若目標Activity的實例已經存在,且位於棧頂,那麼就不會建立新的實例,而是複用已有實例,並依次調用目標Activity的onPause -> onNewIntent -> onResume方法。

OK,下面經過兩個案例詳細分析下SingleTop模式:

案例1,具體步驟以下所示:

  1. 建立MainActivity,而且在onCreate方法中打印出當前Activity所在的任務棧ID,在onNewIntent方法中打印出"onNewIntent"字符串。
  2. 設置MainActivity的啓動模式爲SingleTop。
  3. 經過MainActivity不斷啓動MainActivity自己。

通過上述步驟,最終的任務棧以下所示(adb shell dumpsys activity activities):

SingleTop1

經過MainActivity打印出的日誌以下所示:

SingleTop1日誌

經過上面的任務棧和日誌可知:儘管屢次啓動了MainActivity,但只在第一次的時候建立了實例,後續的屢次啓動,僅僅調用了已有MainActivity實例的onNewIntent方法。最終任務棧(TaskId爲2134)內只有一個MainActivity實例。

此外,還有一點須要注意: 屢次啓動處於棧頂的SingleTop模式的Activity時,其回調函數的順序是onPause -> onNewIntent -> onResume

案例2,具體步驟以下所示:

  1. 建立MainActivity,而且在onCreate方法中打印出當前Activity所在的任務棧ID,在onNewIntent方法中打印出"onNewIntent"字符串。
  2. 建立FirstActivity,而且在onCreate方法中打印出當前Activity所在的任務棧ID,在onNewIntent方法中打印出"onNewIntent"字符串。
  3. 設置MainActivity的啓動模式爲SingleTop,設置FirstActivity的啓動模式爲Standard。
  4. 首先啓動MainActivity,接着經過MainActivity啓動FirstActivity,而後再經過FirstActivity啓動MainActivity,如此反覆。

通過上述步驟,最終的任務棧以下所示(adb shell dumpsys activity activities):

SingleTop2

經過MainActivity打印出的日誌以下所示:

SingleTop2-MainActivity

經過FirstActivity打印出的日誌以下所示:

SingleTop2-FirstActivity

經過上面的任務棧和日誌可知:儘管MainActivity的啓動模式爲SingleTop,可是當它不位於棧頂時,仍然會建立新的實例。最終任務棧中會出現多個MainActivity和FirstActivity,且自始至終,都只有一個任務棧(TaskId爲2068)。

SingleTask

棧內複用模式。該模式是對SingleTop的進一步增強,若Activity實例已經存在,則不論是不是在棧頂,都不會建立新的實例,而是複用已有Activity實例,即清除任務棧中目標Activity之上的全部Activity,使其位於棧頂,同時也會調用其onNewIntent方法;若Activity實例不存在,系統首先會確認是否有目標Activity指望的任務棧,若是沒有,就首先建立目標Activity指望的任務棧,而後建立目標Activity實例並添加到指望的任務棧中;相反,若存在指望的任務棧,那麼就直接建立目標Activity實例並將其添加到指望的任務棧。

而Activity指望的任務棧名稱就是經過上面介紹的android:taskAffinity屬性進行設置的。

OK,針對上述狀況,咱們來看兩個具體案例:

案例1,兩個Activity的taskAffinity屬性相同,具體步驟以下所示:

  1. 建立MainActivity,而且在onCreate方法中打印出當前Activity所在的任務棧ID,在onNewIntent方法中打印出"onNewIntent"字符串。
  2. 建立FirstActivity,而且在onCreate方法中打印出當前Activity所在的任務棧ID,在onNewIntent方法中打印出"onNewIntent"字符串。
  3. 設置MainActivity的啓動模式爲Standard,設置FirstActivity的啓動模式爲SingleTask。
  4. 首先啓動MainActivity,接着經過MainActivity啓動FirstActivity,而後再經過FirstActivity啓動MainActivity,如此反覆。

當第一次經過MainActivity啓動FirstActivity時,任務棧以下所示:

SingleTask案例1-第一次經過MainActivity啓動FirstActivity
可見,由於兩個Activity指望的任務棧相同,所以MainActivity和FirstActivity處於相同的任務棧中(TaskId爲2074)。

而後,經過FirstActivity啓動MainActivity時,任務棧以下所示:

SingleTask案例1-第一次經過FirstActivity啓動MainActivity

最後,再次經過MainActivity啓動FirstActivity時,任務棧以下所示:

SingleTask案例1-再次經過MainActivity啓動FirstActivity
可見,並無爲FirstActivity建立新的實例,而是把FirstActivity之上的MainActivity出棧,直接複用已有FirstActivity實例。

經過MainActivity打印出的日誌以下所示:

SingleTask案例1-MainActivity

經過FirstActivity打印出的日誌以下所示:

SingleTask案例1-FirstActivity

經過上面的任務棧和日誌可知:當兩個Activity的taskAffinity相同時,第一次啓動FirstActivity時,是直接將其實例入棧到MainActivity所在的任務棧(其實MainActivity所在的任務棧就是FirstActivity指望的任務棧);當第二次啓動FirstActivity時,則是直接複用已有FirstActivity實例,同時調用其onNewIntent方法。

案例2,兩個Activity的taskAffinity屬性不一樣,具體步驟以下所示:

  1. 建立MainActivity,而且在onCreate方法中打印出當前Activity所在的任務棧ID,在onNewIntent方法中打印出"onNewIntent"字符串。
  2. 建立FirstActivity,而且在onCreate方法中打印出當前Activity所在的任務棧ID,在onNewIntent方法中打印出"onNewIntent"字符串。
  3. 設置MainActivity的啓動模式爲Standard,設置FirstActivity的啓動模式爲SingleTask。同時設置FirstActivity的android:taskAffinity屬性爲leon.com.activitylaunchmode1,而MainActivity的android:taskAffinity屬性爲默認值,即App包名:leon.com.activitylaunchmode。
  4. 首先啓動MainActivity,接着經過MainActivity啓動FirstActivity,而後再經過FirstActivity啓動MainActivity,如此反覆。

當第一次經過MainActivity啓動FirstActivity時,任務棧以下所示:

SingleTask案例2-第一次經過MainActivity啓動FirstActivity
可見,由於兩個Activity指望的任務棧不一樣,所以系統會爲FirstActivity建立新任務棧(TaskId爲2078)。

而後,經過FirstActivity啓動MainActivity時,任務棧以下所示:

SingleTask案例2-第一次經過FirstActivity啓動MainActivity
由於MainActivity的啓動模式爲Standard,因此其Activity實例會入棧到啓動它的FirstActivity所在的任務棧中。

最後,再次經過MainActivity啓動FirstActivity時,任務棧以下所示:

SingleTask案例2-再次經過MainActivity啓動FirstActivity
可見,並無爲FirstActivity建立新的實例,而是把FirstActivity之上的MainActivity出棧,直接複用已有FirstActivity實例。

經過MainActivity打印出的日誌以下所示:

SingleTask案例2-MainActivity

經過FirstActivity打印出的日誌以下所示:

SingleTask案例2-FirstActivity

經過上面的任務棧和日誌可知:和案例1的惟一不一樣就是第一次啓動FirstActivity時,系統會爲其建立新的任務棧。


總的來講:SingleTask模式與android:taskAffinity屬性相關。以MainActivity啓動FirstActivity爲例(MainActivity爲Standard模式,FirstActivity爲SingleTask模式):

  1. 當MainActivity和FirstActivity的taskAffinity屬性相同時:第一次啓動FirstActivity時,並不會啓動新的任務棧,而是直接將FirstActivity添加到MainActivity所在的任務棧;不然,將FirstActivity所在任務棧中位於FirstActivity之上的所有Activity都刪除,直接跳轉到FirstActivity中。
  1. 當MainActivity和FirstActivity的taskAffinity屬性不一樣時:第一次啓動FirstActivity時,會建立新的任務棧,而後將FirstActivity添加到新的任務棧中;不然,將FirstActivity所在任務棧中位於FirstActivity之上的所有Activity都刪除,直接跳轉到FirstActivity中。

另外,當目標Activity處於棧頂時,啓動SingleTask模式的目標Activity,其回調函數的順序是onPause -> onNewIntent -> onResume,和SingleTop模式相同。

而當目標Activity存在,可是不位於棧頂時,啓動SingleTask模式的目標Activity,其回調函數的順序是onNewIntent -> onRestart -> onStart -> onResume

SingleInstance

單實例模式。該模式是SingleTask的強化,除了具備SingleTask的全部特性外,還強調任意時刻只容許存在惟一的Activity實例,且該Activity實例獨自佔有一個任務棧。即該任務棧只能容納該Activity實例,不能再添加其餘Activity實例到該任務棧,若是該Activity實例已經存在於某個任務棧,則直接跳轉到該任務棧。

OK,下面經過一個案例詳細分析下SingleInstance模式,具體步驟以下所示:

  1. 建立MainActivity,而且在onCreate方法中打印出當前Activity所在的任務棧ID,在onNewIntent方法中打印出"onNewIntent"字符串。
  2. 建立FirstActivity,而且在onCreate方法中打印出當前Activity所在的任務棧ID,在onNewIntent方法中打印出"onNewIntent"字符串。
  3. 設置MainActivity的啓動模式爲Standard,設置FirstActivity的啓動模式爲SingleInstance。
  4. 首先啓動MainActivity,接着經過MainActivity啓動FirstActivity,而後再經過FirstActivity啓動MainActivity,如此反覆。

當第一次經過MainActivity啓動FirstActivity時,任務棧以下所示:

SingleInstance-第一次經過MainActivity啓動FirstActivity
可見,此時FirstActivity處於獨立的任務棧中(TaskId爲2087)。

而後,經過FirstActivity啓動MainActivity時,任務棧以下所示:

SingleInstance-第一次經過FirstActivity啓動MainActivity
可見,新建立的MainActivity實例不是位於FirstActivity實例所處的任務棧,而是位於以前MainActivity實例所處的任務棧(TaskId爲2086)。這也間接證實了SingleInstance模式的Activity是獨佔一個任務棧的。

最後,再次經過MainActivity啓動FirstActivity時,任務棧以下所示:

SingleInstance-再次經過MainActivity啓動FirstActivity
可見,並無爲FirstActivity建立新的實例,僅僅是把TaskId爲2087的任務棧切換到了前臺。

經過MainActivity打印出的日誌以下所示:

SingleInstance-MainActivity

經過FirstActivity打印出的日誌以下所示:

SingleInstance-FirstActivity

經過上面的任務棧和日誌可知:儘管屢次啓動了SingleInstance模式的FirstActivity,但只在第一次的時候建立了Activity實例,而且爲該實例建立了新的任務棧,後續的屢次啓動,僅僅調用了已有FirstActivity實例的onNewIntent方法,並將其任務棧切換到了前臺。 同時,FirstActivity所處任務棧的TaskId爲2087,MainActivity所處任務棧的TaskId爲2086,MainActivity的實例永遠不可能位於FirstActivity所處的任務棧中,即SingleInstance模式的Activity獨佔一個任務棧。

此外,還有一點須要注意: 當目標Activity實例已經存在時,啓動SingleInstance模式的目標Activity,其回調函數的順序是onNewIntent -> onRestart -> onStart -> onResume

特殊案例

上述經過具體案例詳細分析了四種啓動模式,可是還有一些特殊使用場景須要詳細分析下。

針對SingleTask模式的進一步強化

假設如今有兩個任務棧:前臺任務棧中包含ActivityA和ActivityB,後臺任務棧中包含ActivityC和ActivityD,且前臺任務棧中Activity的啓動模式均爲Standard,後臺任務棧中Activity的啓動模式均爲SingleTask,兩個任務棧的棧名是不一樣的。

如今有兩種典型的使用場景: 場景1:當經過前臺任務棧中的ActivityB啓動後臺任務棧中ActivityD時,系統會將後臺任務棧會切換到前臺。此時,當用戶經過「back」鍵退出時,整個退出順序應該是ActivityD -> ActivityC -> ActivityB -> ActivityA

場景2:當經過前臺任務棧中的ActivityB啓動後臺任務棧中ActivityC時,系統會將後臺任務棧會切換到前臺,同時把ActivityC之上的ActivityD出棧。此時,當用戶經過「back」鍵退出時,整個退出順序應該是ActivityC -> ActivityB -> ActivityA

這裏咱們經過具體案例驗證下場景1(場景2比較相似,再也不贅述),具體步驟以下所示:

  1. 首先建立包名爲leon.com.launchmodeab的App1,包含ActivityA和ActivityB,設置其啓動模式爲Standard,而後建立包名爲leon.com.launchmodecd的App2,包含ActivityC和ActivityD,設置其啓動模式爲SingleTask。
  2. 接着啓動App2,並依次啓動ActivityC和ActivityD,而後經過「home」鍵切換到後臺。
  3. 而後啓動App1,並依次啓動ActivityA和ActivityB。
  4. 最後,經過ActivityB啓動ActivityD,並經過「back」鍵依次退出,觀察Activity的退出順序。

當完成步驟1~3時,任務棧以下所示:

前臺任務棧和後臺任務棧都已啓動
可知,TaskIdId爲2146的任務棧包含ActivityA和ActivityB,TaskId爲2145的任務棧包含ActivityA和ActivityB。此時,如果直接經過「back」鍵退出,那麼退出順序是 ActivityB -> ActivityA

當經過ActivityB啓動ActivityD後,任務棧以下所示:

經過ActivityB啓動ActivityD後
可知,包含ActivityD的任務棧被切換到了前臺,雖然ActivityC和ActivityD被隔開了,可是從TaskId和棧名來看,他們仍是是同一個任務棧,而ActivityA和ActivityB所在的棧則在後面。此時,若經過「back」鍵退出,那麼退出順序是 ActivityD -> ActivityC -> ActivityB -> ActivityA

taskAffinity和allowTaskReparenting結合使用場景

android:allowTaskReparenting主要用於Activity的遷移。當allowTaskReparenting爲true時,表示該Activity能從其餘任務棧遷移到指望的任務棧,當allowTaskReparenting的值爲「false」,表示該Activity不能從其餘任務棧進行遷移,默認爲false。這個比較抽象,咱們看一個具體的案例,具體步驟以下所示:

  1. 首先建立包名爲leon.com.launchmodeab的App1,包含ActivityA,設置其啓動模式爲Standard,而後建立包名爲leon.com.launchmodecd的App2,包含ActivityC,設置其啓動模式爲Standard。
  2. 接着啓動App1,並經過App1的ActivityA啓動App2的ActivityC,,而後經過「home」鍵切換到後臺。
  3. 而後啓動App2,查看任務棧狀況。

OK,首先來看下ActivityC的allowTaskReparenting屬性爲true的狀況。 完成上述1~2步驟後的任務棧以下所示:

Activity遷移1-2
可見,此時ActivityC和ActivityA處於同一個任務棧。

完成第3步後的任務棧以下所示:

Activity遷移3
可見,此時ActivityC從TaskId爲2166的任務棧遷移到了TaskId爲2167的任務棧中,符合咱們的預期。

而後看下ActivityC的allowTaskReparenting屬性爲false的狀況。 完成上述1~2步驟後的任務棧以下所示:

Activity不遷移1-2
可見,此時ActivityC和ActivityA處於同一個任務棧。

完成第3步後的任務棧以下所示:

Activity不遷移3
可見,原來的ActivityC實例並無被遷移,而是系統爲ActivityC建立了新的實例,也符合咱們的預期。


先介紹這兩個特殊使用場景,後面會陸續補充...

參考文章

  1. Activity啓動模式與任務棧(Task)全面深刻記錄(上)
  2. Activity啓動模式與任務棧(Task)全面深刻記錄(下)
  3. Android 之Activity啓動模式(一)之 lauchMode
  4. Android 之Activity啓動模式(二)之 Intent的Flag屬性
  5. Android 之Activity啓動模式(三)之 啓動模式的其它屬性
相關文章
相關標籤/搜索