一,前言
有時候會有這樣的需求,例如,一個閱讀app,主界面(Activity A)是一個顯示各種小說的列表,我們選擇一本小說,來到該小說的目錄列表界面(Activity B),然后我們選擇了某一章進入到小說的內容界面(Activity C)開始閱讀,這時候我們覺得這本小說沒意思,想換別的小說看看,然后想不經過中間的目錄列表(Activity B)立馬回到app的主界面(Activity A)去重新選擇一本小說。大家可能有各種辦法去實現,那么通過下面Activity啟動模式的學習,就會明白如何解決這種需求了。
二,任務和返回棧
一個應用程序當中通常都會包含很多個Activity,每個Activity都應該設計成為一個具有特定的功能,并且可以讓用戶進行操作的組件。另外,Activity之間還應該是可以相互啟動的。比如,一個郵件應用中可能會包含一個用于展示郵件列表的Activity,而當用戶點擊了其中某一封郵件的時候,就會打開另外一個Activity來顯示該封郵件的具體內容。
除此之外,一個Activity甚至還可以去啟動其它應用程序當中的Activity。打個比方,如果你的應用希望去發送一封郵件,你就可以定義一個具有"send"動作的Intent,并且傳入一些數據,如對方郵箱地址、郵件內容等。這樣,如果另外一個應用程序中的某個Activity聲明自己是可以響應這種Intent的,那么這個Activity就會被打開。在當前場景下,這個Intent是為了要發送郵件的,所以說郵件應用程序當中的編寫郵件Activity就應該被打開。當郵件發送出去之后,仍然還是會回到你的應用程序當中,這讓用戶看起來好像剛才那個編寫郵件的Activity就是你的應用程序當中的一部分。所以說,即使有很多個Activity分別都是來自于不同應用程序的,Android系統仍然可以將它們無縫地結合到一起,之所以能實現這一點,就是因為這些Activity都是存在于一個相同的任務(Task)當中的。
任務是一個Activity的集合,它使用棧的方式來管理其中的Activity,這個棧又被稱為返回棧(back stack),棧中Activity的順序就是按照它們被打開的順序依次存放的。
手機的Home界面是大多數任務開始的地方,當用戶在Home界面上點擊了一個應用的圖標時,這個應用的任務就會被轉移到前臺。如果這個應用目前并沒有任何一個任務的話(說明這個應用最近沒有被啟動過),系統就會去創建一個新的任務,并且將該應用的主Activity放入到返回棧當中。
當一個Activity啟動了另外一個Activity的時候,新的Activity就會被放置到返回棧的棧頂并將獲得焦點。前一個Activity仍然保留在返回棧當中,但會處于停止狀態。當用戶按下Back鍵的時候,棧中最頂端的Activity會被移除掉,然后前一個Activity則會得重新回到最頂端的位置。返回棧中的Activity的順序永遠都不會發生改變,我們只能向棧頂添加Activity,或者將棧頂的Activity移除掉。因此,返回棧是一個典型的后進先出(last in, first out)的數據結構。下圖通過時間線的方式非常清晰地向我們展示了多個Activity在返回棧當中的狀態變化:
如果用戶一直地按Back鍵,這樣返回棧中的Activity會一個個地被移除,直到最終返回到主屏幕。當返回棧中所有的Activity都被移除掉的時候,對應的任務也就不存在了。
任務除了可以被轉移到前臺之外,當然也是可以被轉移到后臺的。當用戶開啟了一個新的任務,或者點擊Home鍵回到主屏幕的時候,之前任務就會被轉移到后臺了。當任務處于后臺狀態的時候,返回棧中所有的Activity都會進入停止狀態,但這些Activity在棧中的順序都會原封不動地保留著,如下圖所示:
這個時候,用戶還可以將任意后臺的任務切換到前臺,這樣用戶應該就會看到之前離開這個任務時處于最頂端的那個Activity。舉個例子來說,當前任務A的棧中有三個Activity,現在用戶按下Home鍵,然后點擊桌面上的圖標啟動了另外一個應用程序。當系統回到桌面的時候,其實任務A就已經進入后臺了,然后當另外一個應用程序啟動的時候,系統會為這個程序開啟一個新的任務(任務B)。當用戶使用完這個程序之后,再次按下Home鍵回到桌面,這個時候任務B也進入了后臺。然后用戶又重新打開了第一次使用的程序,這個時候任務A又會回到前臺,A任務棧中的三個Activity仍然會保留著剛才的順序,最頂端的Activity將重新變為運行狀態。之后用戶仍然可以通過Home鍵或者多任務鍵來切換回任務B,或者啟動更多的任務,這就是Android中多任務切換的例子。
由于返回棧中的Activity的順序永遠都不會發生改變,所以如果你的應用程序中允許有多個入口都可以啟動同一個Activity,那么每次啟動的時候就都會創建該Activity的一個新的實例,而不是將下面的Activity的移動到棧頂。這樣的話就容易導致一個問題的產生,即同一個Activity有可能會被實例化很多次,如下圖所示:
但是呢,如果你不希望同一個Activity可以被多次實例化,那當然也是可以的,馬上我們就將開始討論如果實現這一功能,現在我們先把默認的任務和Activity的行為簡單概括一下:
- 當Activity A啟動Activity B時,Activity A進入停止狀態,但系統仍然會將它的所有相關信息保留,比如滾動的位置,還有文本框輸入的內容等。如果用戶在Activity B中按下Back鍵,那么Activity A將會重新回到運行狀態。
- 當用戶通過Home鍵離開一個任務時,該任務會進入后臺,并且返回棧中所有的Activity都會進入停止狀態。系統會將這些Activity的狀態進行保留,這樣當用戶下一次重新打開這個應用程序時,就可以將后臺任務直接提取到前臺,并將之前最頂端的Activity進行恢復。
- 當用戶按下Back鍵時,當前最頂端的Activity會被從返回棧中移除掉,移除掉的Activity將被銷毀,然后前面一個Activity將處于棧頂位置并進入活動狀態。當一個Activity被銷毀了之后,系統不會再為它保留任何的狀態信息。
- 每個Activity都可以被實例化很多次,即使是在不同的任務當中。
三,管理任務
1,定義啟動模式
使用manifest文件
standard(默認啟動模式)
standard是默認的啟動模式,即如果不指定launchMode屬性,則自動就會使用這種啟動模式。這種啟動模式表示每次啟動該Activity時系統都會為創建一個新的實例,并且總會把它放入到當前的任務當中。聲明成這種啟動模式的Activity可以被實例化多次,一個任務當中也可以包含多個這種Activity的實例。
singleTop
這種啟動模式表示,如果要啟動的這個Activity在當前任務中已經存在了,并且還處于棧頂的位置,那么系統就不會再去創建一個該Activity的實例,而是調用棧頂Activity的onNewIntent()方法。聲明成這種啟動模式的Activity也可以被實例化多次,一個任務當中也可以包含多個這種Activity的實例。
舉個例子來講,一個任務的返回棧中有A、B、C、D四個Activity,其中A在最底端,D在最頂端。這個時候如果我們要求再啟動一次D,并且D的啟動模式是"standard",那么系統就會再創建一個D的實例放入到返回棧中,此時棧內元素為:A-B-C-D-D。而如果D的啟動模式是"singleTop"的話,由于D已經是在棧頂了,那么系統就不會再創建一個D的實例,而是直接調用D Activity的onNewIntent()方法,此時棧內元素仍然為:A-B-C-D。
singleTask
這種啟動模式表示,系統會創建一個新的任務,并將啟動的Activity放入這個新任務的棧底位置。但是,如果現有任務當中已經存在一個該Activity的實例了,那么系統就不會再創建一次它的實例,而是會直接調用它的onNewIntent()方法。聲明成這種啟動模式的Activity,在同一個任務當中只會存在一個實例。注意這里我們所說的啟動Activity,都指的是啟動其它應用程序中的Activity,因為"singleTask"模式在默認情況下只有啟動其它程序的Activity才會創建一個新的任務,啟動自己程序中的Activity還是會使用相同的任務,具體原因會在下面 處理關聯affinity部分進行解釋。
singleInstance
這種啟動模式和"singleTask"有點相似,只不過系統不會向聲明成"singleInstance"的Activity所在的任務當中再添加其它Activity。也就是說,這種Activity所在的任務中始終只會有一個Activity,通過這個Activity再打開的其它Activity也會被放入到別的任務當中。
使用Intent flags
除了使用manifest文件之外,你也可以在調用startActivity()方法的時候,為Intent加入一個flag來改變Activity與任務的關聯方式,下面我們來一一講解一下每種flag的作用:
FLAG_ACTIVITY_NEW_TASK
設置了這個flag,新啟動Activity就會被放置到一個新的任務當中(與"singleTask"有點類似,但不完全一樣),當然這里討論的仍然還是啟動其它程序中的Activity。這個flag的作用通常是模擬一種Launcher的行為,即列出一推可以啟動的東西,但啟動的每一個Activity都是在運行在自己獨立的任務當中的。
FLAG_ACTIVITY_SINGLE_TOP
設置了這個flag,如果要啟動的Activity在當前任務中已經存在了,并且還處于棧頂的位置,那么就不會再次創建這個Activity的實例,而是直接調用它的onNewIntent()方法。這種flag和在launchMode中指定"singleTop"模式所實現的效果是一樣的。
FLAG_ACTIVITY_CLEAR_TOP
設置了這個flag,如果要啟動的Activity在當前任務中已經存在了,就不會再次創建這個Activity的實例,而是會把這個Activity之上的所有Activity全部關閉掉。比如說,一個任務當中有A、B、C、D四個Activity,然后D調用了startActivity()方法來啟動B,并將flag指定成FLAG_ACTIVITY_CLEAR_TOP,那么此時C和D就會被關閉掉,現在返回棧中就只剩下A和B了。
那么此時Activity B會接收到這個啟動它的Intent,你可以決定是讓Activity B調用onNewIntent()方法(不會創建新的實例),還是將Activity B銷毀掉并重新創建實例。如果Activity B沒有在manifest中指定任何啟動模式(也就是"standard"模式),并且Intent中也沒有加入一個FLAG_ACTIVITY_SINGLE_TOP flag,那么此時Activity B就會銷毀掉,然后重新創建實例。而如果Activity B在manifest中指定了任何一種啟動模式,或者是在Intent中加入了一個FLAG_ACTIVITY_SINGLE_TOP flag,那么就會調用Activity B的onNewIntent()方法。
FLAG_ACTIVITY_CLEAR_TOP和FLAG_ACTIVITY_NEW_TASK結合在一起使用也會有比較好的效果,比如可以將一個后臺運行的任務切換到前臺,并把目標Activity之上的其它Activity全部關閉掉。這個功能在某些情況下非常有用,比如說從通知欄啟動Activity的時候。
2,處理關聯(affinity)
affinity可以用于指定一個Activity更加愿意依附于哪一個任務,在默認情況下,同一個應用程序中的所有Activity都具有相同的affinity,所以,這些Activity都更加傾向于運行在相同的任務當中。當然了,你也可以去改變每個Activity的affinity值,通過<activity>元素的taskAffinity屬性就可以實現了。
taskAffinity屬性接收一個字符串參數,你可以指定成任意的值(經我測試字符串中至少要包含一個.),但必須不能和應用程序的包名相同,因為系統會使用包名來作為默認的affinity值。
affinity主要有以下兩種應用場景:
當調用startActivity()方法來啟動一個Activity時,默認是將它放入到當前的任務當中。但是,如果在Intent中加入了一個FLAG_ACTIVITY_NEW_TASK flag的話(或者該Activity在manifest文件中聲明的啟動模式是"singleTask"),系統就會嘗試為這個Activity單獨創建一個任務。但是規則并不是只有這么簡單,系統會去檢測要啟動的這個Activity的affinity和當前任務的affinity是否相同,如果相同的話就會把它放入到現有任務當中,如果不同則會去創建一個新的任務。而同一個程序中所有Activity的affinity默認都是相同的,這也是前面為什么說,同一個應用程序中即使聲明成"singleTask",也不會為這個Activity再去創建一個新的任務了。
當把Activity的allowTaskReparenting屬性設置成true時,Activity就擁有了一個轉移所在任務的能力。具體點來說,就是一個Activity現在是處于某個任務當中的,但是它與另外一個任務具有相同的affinity值,那么當另外這個任務切換到前臺的時候,該Activity就可以轉移到現在的這個任務當中。
那還是舉一個形象點的例子吧,比如有一個天氣預報程序,它有一個Activity是專門用于顯示天氣信息的,這個Activity和該天氣預報程序的所有其它Activity具體相同的affinity值,并且還將allowTaskReparenting屬性設置成true了。這個時候,你自己的應用程序通過Intent去啟動了這個用于顯示天氣信息的Activity,那么此時這個Activity應該是和你的應用程序是在同一個任務當中的。但是當把天氣預報程序切換到前臺的時候,這個Activity又會被轉移到天氣預報程序的任務當中,并顯示出來,因為它們擁有相同的affinity值,并且將allowTaskReparenting屬性設置成了true。
3,清理返回棧
如何用戶將任務切換到后臺之后過了很長一段時間,系統會將這個任務中除了最底層的那個Activity之外的其它所有Activity全部清除掉。當用戶重新回到這個任務的時候,最底層的那個Activity將得到恢復。這個是系統默認的行為,因為既然過了這么長的一段時間,用戶很有可能早就忘記了當時正在做什么,那么重新回到這個任務的時候,基本上應該是要去做點新的事情了。
當然,既然說是默認的行為,那就說明我們肯定是有辦法來改變的,在<activity>元素中設置以下幾種屬性就可以改變系統這一默認行為:
alwaysRetainTaskState
如果將最底層的那個Activity的這個屬性設置為true,那么上面所描述的默認行為就將不會發生,任務中所有的Activity即使過了很長一段時間之后仍然會被繼續保留。
clearTaskOnLaunch
如果將最底層的那個Activity的這個屬性設置為true,那么只要用戶離開了當前任務,再次返回的時候就會將最底層Activity之上的所有其它Activity全部清除掉。簡單來講,就是一種和alwaysRetainTaskState完全相反的工作模式,它保證每次返回任務的時候都會是一種初始化狀態,即使用戶僅僅離開了很短的一段時間。
finishOnTaskLaunch
這個屬性和clearTaskOnLaunch是比較類似的,不過它不是作用于整個任務上的,而是作用于單個Activity上。如果某個Activity將這個屬性設置成true,那么用戶一旦離開了當前任務,再次返回時這個Activity就會被清除掉。
四,開始任務
通過為 Activity 提供一個以 "android.intent.action.MAIN"
為指定操作,
以"android.intent.category.LAUNCHER"
為指定類別的 Intent 過濾器,您可以將Activity設置為任務的入口點。 例如:
<activity ... >
<intent-filter ... >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
...
</activity>
此類 Intent 過濾器會使 Activity 的圖標和標簽顯示在應用啟動器中,讓用戶能夠啟動 Activity 并在啟動之后隨時返回到創建的任務中。
第二個功能非常重要:用戶必須能夠在離開任務后,再使用此 Activity 啟動器返回該任務。 因此,只有在 Activity 具有 ACTION_MAIN
和CATEGORY_LAUNCHER
過濾器時,才應該使用將 Activity 標記為“始終啟動任務”的兩種啟動模式,即 "singleTask"
和 "singleInstance"
。例如,我們可以想像一下如果缺少過濾器會發生什么情況: Intent 啟動一個 "singleTask"
Activity,從而啟動一個新任務,并且用戶花了些時間處理該任務。然后,用戶按主頁按鈕。 任務現已發送到后臺,而且不可見。現在,用戶無法返回到任務,因為該任務未顯示在應用啟動器中。
如果您并不想用戶能夠返回到 Activity,對于這些情況,請將 <activity>
元素的 finishOnTaskLaunch
設置為 "true"
感謝郭霖的好文章:Android任務和返回棧完全解析,細數那些你所不知道的細節
google官方文檔地址:Understand Tasks and Back Stack
進程和任務的區別:進程(Processes)和任務(tasks)的區別