Android四大組件Activity啟動模式完全解析

一,前言

有時候會有這樣的需求,例如,一個閱讀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在返回棧當中的狀態變化:

image

如果用戶一直地按Back鍵,這樣返回棧中的Activity會一個個地被移除,直到最終返回到主屏幕。當返回棧中所有的Activity都被移除掉的時候,對應的任務也就不存在了。

任務除了可以被轉移到前臺之外,當然也是可以被轉移到后臺的。當用戶開啟了一個新的任務,或者點擊Home鍵回到主屏幕的時候,之前任務就會被轉移到后臺了。當任務處于后臺狀態的時候,返回棧中所有的Activity都會進入停止狀態,但這些Activity在棧中的順序都會原封不動地保留著,如下圖所示:

image

這個時候,用戶還可以將任意后臺的任務切換到前臺,這樣用戶應該就會看到之前離開這個任務時處于最頂端的那個Activity。舉個例子來說,當前任務A的棧中有三個Activity,現在用戶按下Home鍵,然后點擊桌面上的圖標啟動了另外一個應用程序。當系統回到桌面的時候,其實任務A就已經進入后臺了,然后當另外一個應用程序啟動的時候,系統會為這個程序開啟一個新的任務(任務B)。當用戶使用完這個程序之后,再次按下Home鍵回到桌面,這個時候任務B也進入了后臺。然后用戶又重新打開了第一次使用的程序,這個時候任務A又會回到前臺,A任務棧中的三個Activity仍然會保留著剛才的順序,最頂端的Activity將重新變為運行狀態。之后用戶仍然可以通過Home鍵或者多任務鍵來切換回任務B,或者啟動更多的任務,這就是Android中多任務切換的例子。

由于返回棧中的Activity的順序永遠都不會發生改變,所以如果你的應用程序中允許有多個入口都可以啟動同一個Activity,那么每次啟動的時候就都會創建該Activity的一個新的實例,而不是將下面的Activity的移動到棧頂。這樣的話就容易導致一個問題的產生,即同一個Activity有可能會被實例化很多次,如下圖所示:

image

但是呢,如果你不希望同一個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_MAINCATEGORY_LAUNCHER 過濾器時,才應該使用將 Activity 標記為“始終啟動任務”的兩種啟動模式,即 "singleTask""singleInstance"。例如,我們可以想像一下如果缺少過濾器會發生什么情況: Intent 啟動一個 "singleTask" Activity,從而啟動一個新任務,并且用戶花了些時間處理該任務。然后,用戶按主頁按鈕。 任務現已發送到后臺,而且不可見。現在,用戶無法返回到任務,因為該任務未顯示在應用啟動器中。

如果您并不想用戶能夠返回到 Activity,對于這些情況,請將 <activity> 元素的 finishOnTaskLaunch設置為 "true"

感謝郭霖的好文章:Android任務和返回棧完全解析,細數那些你所不知道的細節
google官方文檔地址:Understand Tasks and Back Stack
進程和任務的區別:進程(Processes)和任務(tasks)的區別

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容