在開發中,與界面跳轉聯系比較緊密的概念是Task(任務)和Back Stack(回退棧)。activity的啟動模式會影響Task和Back Stack的狀態, 進而影響用戶體驗。除了啟動模式之外,Intent類中定義的一些標志(以FLAG_ACTIVITY_開頭)也會影響Task和Back Stack的狀態。 在這篇文章中主要對android的堆棧管理進行分析和驗證,其中涉及到activity的一個重要屬性taskAffinity和Intent中的標志之一FLAG_ACTIVITY_NEW_TASK。
Task和Back Stack簡介
task(任務)是一組Activities的集合,一組Activities被Stack(back stack)所管理,棧中Activity的順序就是按照它們被打開的順序依次存放的。
手機的Home界面是大多數task開始的地方,當用戶在Home界面上點擊了一個應用的圖標時,這個應用的task就會被轉移到前台。 如果這個應用目前並沒有任何一個任務的話(說明這個應用最近沒有被啟動過),系統就會去創建一個新的task, 並且將該應用的主Activity放入到返回棧當中。
當一個Activity啟動了另外一個Activity的時候,新的Activity就會被放置到返回棧的棧頂並將獲得焦點。 前一個Activity仍然保留在返回棧當中,但會處於停止狀態。當用戶按下Back鍵的時候,棧中最頂端的Activity會被移除掉, 然后前一個Activity則會得重新回到最頂端的位置。返回棧中的Activity的順序永遠都不會發生改變, 我們只能向棧頂添加Activity,或者將棧頂的Activity移除掉。因此,返回棧是一個典型的后進先出(last in, first out)的數據結構。
task是可以跨應用的,這正是task存在的一個重要原因。有的Activity,雖然不在同一個app中,但為了保持用戶操作的連貫性, 把他們放在同一個任務中。例如,在我們的應用中的一個Activity A中點擊發送郵件,會啟動郵件程序的一個Activity B來發送郵件, 這兩個activity是存在於不同app中的,但是被系統放在一個任務中,這樣當發送完郵件后,用戶按back鍵返回,可以返回到原來的Activity A中, 這樣就確保了用戶體驗。
下面來用代碼做一個驗證: 首先:我們來啟動三個Activity來模擬生成活動與任務堆棧,三個Activity分別是:AndroidStackTaskActivity1、 AndroidStackTaskActivity2、AndroidStackTaskActivity3,具體代碼如果下:
[代碼]java代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
public
class
AndroidStackTaskActivity1
extends
Activity
implements
OnClickListener{
private
Button next =
null
;
@Override
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.main);
init();
}
private
void
init(){
next = (Button)findViewById(R.id.button1);
next.setOnClickListener(
this
);
}
@Override
public
void
onClick(View v) {
Intent i =
new
Intent(
this
,AndroidStackTaskActivity2.
class
);
startActivity(i);
}
}
public
class
AndroidStackTaskActivity2
extends
Activity
implements
OnClickListener {
private
Button next =
null
;
@Override
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.main2);
init();
}
private
void
init(){
next = (Button)findViewById(R.id.button1);
next.setOnClickListener(
this
);
}
@Override
public
void
onClick(View v) {
// TODO Auto-generated method stub
Intent i =
new
Intent(
this
,AndroidStackTaskActivity3.
class
);
startActivity(i);
}
}
public
class
AndroidStackTaskActivity3
extends
Activity
implements
OnClickListener {
private
Button next =
null
;
@Override
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.main2);
init();
}
private
void
init(){
next = (Button)findViewById(R.id.button1);
next.setOnClickListener(
this
);
}
@Override
public
void
onClick(View v) {
// TODO Auto-generated method stub
Intent i =
new
Intent(
this
,AndroidStackTaskActivity3.
class
);
startActivity(i);
}
}
|
此時,生成的活動堆棧如下圖所示:
首先activity1被start,此時,如果應用沒有創建task則創建,並把activity1壓入棧頂,activity1觸發onCreate->onStart->onResume。
接着activity1轉向到activity2時,activity1先觸發onPause,activity2觸發onCreate->onStart->onResume,然后activity1觸發onPause->onStop,activity2壓入棧頂。
以此類推,activity2轉向activity3也是一樣的步驟。那么當前棧頂是activity3。
當我們按下手機上的返回鍵時,棧頂的activity3觸發onPause,activity2需要從狀態stop到pause,所以觸發了onPause->onStart->onResume, activity3觸發onStop->onDestory,因為activity3從棧頂彈出,所以觸發onDestory,此時,activity2在棧頂。
如果繼續按返回鍵,當前棧頂的activity彈出並被destory,直到home界面。當所有的activity都彈出了,這個task也就消亡了。
當開始一個新的task時,前一個task被設置為后台。在后台,所有的activity都處理stop狀態,但是back stack保留了所有后台activity的狀態信息,只是丟失了焦點。
這個時候,用戶還可以將任意后台的任務切換到前台,這樣用戶應該就會看到之前離開這個task時處於最頂端的那個Activity。
由於返回棧中的Activity的順序永遠都不會發生改變,所以如果你的應用程序中允許有多個入口都可以啟動同一個Activity, 那么每次啟動的時候就都會創建該Activity的一個新的實例,而不是將下面的Activity的移動到棧頂。這樣的話就容易導致一個問題的產生, 即同一個Activity有可能會被實例化很多次,如下圖所示:
但是呢,如果你不希望同一個Activity可以被多次實例化,這些功能甚至更多功能, 都是可以通過在manifest文件中設置元素的屬性,或者是在啟動Activity時配置Intent的flag來實現的。
下面我們就將開始討論,如何通過manifest參數,以及Intent flag來改變Activity在任務中的默認行為。
1.使用manifest文件
當你在manifest文件中聲明一個Activity的時候,你可以指定這個Activity在啟動的時候該如何與任務進行關聯。
2.使用Intent flag
當你調用startActivity()方法時,你可以在Intent中加入一個flag,從而指定新啟動的Activity該如何與當前任務進行關聯。 也就是說,如果Activity A啟動了Activity B,Activity B可以定義自己該如何與當前任務進行關聯, 而Activity A也可以要求Activity B該如何與當前任務進行關聯。如果Activity B在manifest中已經定義了該如何與任務進行關聯, 而Activity A同時也在Intent中要求了Activity B該怎么樣與當前任務進行關聯,那么此時Intent中的定義將覆蓋manifest中的定義。
需要注意的是,有些啟動模式在manifest中可以指定,但在Intent中是指定不了的。同樣,也有些啟動模式在Intent中可以指定,但在manifest中是指定不了的.
使用manifest文件
當在manifest文件中定義Activity的時候,你可以通過元素的launchMode屬性來指定這個Activity應該如何與任務進行關聯。 launchMode屬性一共有以下四種可選參數:standard,singleTop,singleTask,singleInstance.之前已經詳細介紹過這些內容,這里不在分析。
使用Intent flags
除了使用manifest文件之外,你也可以在調用startActivity()方法的時候,為Intent加入一個flag來改變Activity與任務的關聯方式, 下面我們來一一講解一下每種flag的作用:
1.FLAG_ACTIVITY_NEW_TASK
設置了這個flag,新啟動Activity就會被放置到一個新的task當中(與”singleTask”有點類似,但不完全一樣) ,當然這里討論的仍然還是啟動其它程序中的Activity。這個flag的作用通常是模擬一種Launcher的行為, 即列出一堆可以啟動的東西,但啟動的每一個Activity都是在運行在自己獨立的任務當中的。
如果傳遞給startActivity()的Intent對象包含了FLAG_ACTIVITY_NEW_TASK標記,系統會為新Activity安排另外一個任務。 一般情況下,如同標記所暗示的那樣,這會是一個新任務。然而,這並不是必然的。如果已經存在了一個與新Activity有着同樣affinity的任務 ,則Activity會載入那個任務之中。如果沒有,則啟用新任務。簡言之:有相同affinity的任務,則壓入該任務,否則創建一個新的任務。
2.FLAG_ACTIVITY_SINGLE_TOP
設置了這個flag,如果要啟動的Activity在當前任務中已經存在了,並且還處於棧頂的位置,那么就不會再次創建這個Activity的實例, 而是直接調用它的onNewIntent()方法。這種flag和在launchMode中指定”singleTop”模式所實現的效果是一樣的。
3.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的時候。
以上只介紹了常有用的幾種控制Activity跳轉的Flag標識.
主要的屬性
主要的屬性有: launchMode. taskAffinity. allowTaskReparenting. alwaysRetainTaskState. clearTaskOnLaunch . finishOnTaskLaunch.
下面將對每一個屬性和標志一一介紹:
affinity
通常來說一個程序內/任務棧中的Activity具有親和力,也就是說具有相同親和力的Activity默認屬於同一個任務Task中.
affinity可以用於指定一個Activity更加願意依附於哪一個任務,在默認情況下,同一個應用程序中的所有Activity都具有相同的affinity, 所以,這些Activity都更加傾向於運行在相同的任務當中。當然了,你也可以去改變每個Activity的affinity值, 通過元素的taskAffinity屬性就可以實現了。
taskAffinity屬性接收一個字符串參數,你可以指定成任意的值(字符串中至少要包含一個.),但必須不能和應用程序的包名相同,因為系統會使用包名來作為默認的affinity值。
affinity決定兩件事情——Activity重新宿主(從一個Task跳到了另一個Task中,新的Task就被稱為重新宿主)的Task(參考allowTaskReparenting特性)和使用FLAG_ACTIVITY_NEW_TASK標志啟動的Activity宿主的Task。 注意:affinity只有在加載activity的Intent對象包含了FLAG_ACTIVITY_NEW_TASK 標記,或者當activity的allowTaskReparenting屬性設置為“true”時才有效。
affinity主要有以下應用場景: 當調用startActivity()方法來啟動一個Activity時,默認是將它放入到當前的任務當中。但是, 如果在Intent中加入了一個FLAG_ACTIVITY_NEW_TASK flag的話(或者該Activity在manifest文件中聲明的啟動模式是”singleTask”), 系統就會嘗試為這個Activity單獨創建一個任務。但是規則並不是只有這么簡單,系統會去檢測要啟動的這個Activity的affinity和當前任務的affinity是否相同, 如果相同的話就會把它放入到現有任務當中,如果不同則會去創建一個新的任務。而同一個程序中所有Activity的affinity默認都是相同的, 這也是前面為什么說,同一個應用程序中即使聲明成”singleTask”,也不會為這個Activity再去創建一個新的任務了。
allowTaskReparenting
當把Activity的allowTaskReparenting屬性設置成true時,Activity就擁有了一個轉移所在任務的能力。 具體點來說,就是一個Activity現在是處於某個任務當中的,但是它與另外一個任務具有相同的affinity值, 那么當另外這個任務切換到前台的時候,該Activity就可以轉移到現在的這個任務當中。
那還是舉一個形象點的例子吧,比如有一個天氣預報程序,它有一個Activity是專門用於顯示天氣信息的, 這個Activity和該天氣預報程序的所有其它Activity具體相同的affinity值,並且還將allowTaskReparenting屬性設置成true了。 這個時候,你自己的應用程序通過Intent去啟動了這個用於顯示天氣信息的Activity, 那么此時這個Activity應該是和你的應用程序是在同一個任務當中的。但是當把天氣預報程序切換到前台的時候, 這個Activity又會被轉移到天氣預報程序的任務當中,並顯示出來,因為它們擁有相同的affinity值, 並且將allowTaskReparenting屬性設置成了true。
一般來說,當Activity啟動后,它就與啟動它的Task關聯,並且在那里耗盡它的整個生命周期。 當當前的Task不再顯示時,你可以使用這個特性來強制Activity移動到有着affinity的Task中。典型用法是: 把一個應用程序的Activity移到另一個應用程序的主Task中。
alwaysRetainTaskState,clearTaskOnLaunch,finishOnTaskLaunch可以放在一起討論。 如何用戶將任務切換到后台之后過了很長一段時間,系統會將這個任務中除了最底層的那個Activity之外的其它所有Activity全部清除掉。 當用戶重新回到這個任務的時候,最底層的那個Activity將得到恢復。這個是系統默認的行為,因為既然過了這么長的一段時間, 用戶很有可能早就忘記了當時正在做什么,那么重新回到這個任務的時候,基本上應該是要去做點新的事情了。
當然,既然說是默認的行為,那就說明我們肯定是有辦法來改變的,在元素中設置以下幾種屬性就可以改變系統這一默認行為:
alwaysRetainTaskState
如果將最底層的那個Activity的這個屬性設置為true,那么上面所描述的默認行為就將不會發生,任務中所有的Activity 即使過了很長一段時間之后仍然會被繼續保留。
一般來說,特定的情形如當用戶從主畫面重新選擇這個Task時,系統會對這個Task進行清理(從stack中刪除位於根Activity之上的所有Activivity)。 典型的情況,當用戶有一段時間沒有訪問這個Task時也會這么做,例如30分鍾。 然而,當這個特性設為“true”時,用戶總是能回到這個Task的最新狀態,無論他們是如何啟動的。這非常有用, 例如,像Browser應用程序,這里有很多的狀態(例如多個打開的Tab),用戶不想丟失這些狀態。系統會為我們保持這些狀態數據。
clearTaskOnLaunch
如果將最底層的那個Activity的這個屬性設置為true,那么只要用戶離開了當前任務, 再次返回的時候就會將最底層Activity之上的所有其它Activity全部清除掉。簡單來講,就是一種和alwaysRetainTaskState完全相反的工作模式, 它保證每次返回任務的時候都會是一種初始化狀態,即使用戶僅僅離開了很短的一段時間。 這個特性只對啟動一個新的Task的Activity(根Activity)有意義;對Task中其它的Activity忽略。
假設,某人從主畫面啟動了ActivityP,並從那里遷移至Activity Q。接下來用戶按下HOME,然后返回Activity P。 一般,用戶可能見到的是Activity Q,因為它是P的Task中最后工作的內容。然而,如果P設定這個特性為“true”, 當用戶按下HOME並使這個Task再次進入前台時,其上的所有的Activity(在這里是Q)都將被清除。因此,當返回到這個Task時, 用戶只能看到P。如果這個特性和allowTaskReparenting都設定為“true”,那些能重新宿主的Activity會移動到共享affinity的Task中; 剩下的Activity都將被拋棄。
finishOnTaskLaunch
這個屬性和clearTaskOnLaunch是比較類似的,不過它不是作用於整個任務上的,而是作用於單個Activity上。 如果某個Activity將這個屬性設置成true,那么用戶一旦離開了當前任務,再次返回時這個Activity就會被清除掉。 如果這個特性和allowTaskReparenting都設定為“true”,這個特性勝出,Activity的affinity忽略。這個Activity不會重新宿主,但是會銷毀。
Task和process的區別
process一般翻譯成進程,進程是操作系統內核中的一個概念,表示直接受內核調度的執行單位。在應用程序的角度看,我們用java編寫的應用程序, 運行在dalvik虛擬機中,可以認為一個運行中的dalvik虛擬機實例占有一個進程,所以,在默認情況下, 一個應用程序的所有組件運行在同一個進程中。但是這種情況也有例外,即,應用程序中的不同組件可以運行在不同的進程中。 只需要在manifest中用process屬性指定組件所運行的進程的名字。如下所示:
[代碼]xml代碼:
1
2
|
<
activity
android:name
=
".MyActivity"
android:label
=
"@string/app_nam"
android:process
=
":remote"
>
</
activity
>
|
這樣的話這個activity會運行在一個獨立的進程中。
task是可以跨應用的,這正是task存在的一個重要原因。有的Activity,雖然不在同一個app中,但為了保持用戶操作的連貫性,把他們放在同一個任務中。 task不僅可以跨應用(Application),還可以跨進程(Process)。
關於onNewIntent()
launchMode為singleTask的時候,通過Intent啟到一個Activity,如果系統已經存在一個實例,系統就會將請求發送到這個實例上, 但這個時候,系統就不會再調用通常情況下我們處理請求數據的onCreate方法,而是調用onNewIntent方法.
launchMode為singleTop的時候,如果IntentActivity處於任務棧的頂端,也就是說之前打開過的Activity,現在處於onPause、onStop 狀態的話, 其他應用再發送Intent的話,執行順序為:onNewIntent,onRestart,onStart,onResume。
不要忘記,系統可能會隨時殺掉后台運行的 Activity ,如果這一切發生,那么系統就會調用 onCreate 方法,而不調用 onNewIntent 方法,一個好的解決方法就是在 onCreate 和 onNewIntent 方法中調用同一個處理數據的方法,如下所示:
[代碼]java代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.main);
processExtraData();
}
protected
void
onNewIntent(Intent intent) {
super
.onNewIntent(intent);
setIntent(intent);
//must store the new intent unless getIntent() will return the old one
processExtraData()
}
private
void
processExtraData(){
Intent intent = getIntent();
//use the data received here
}
|
注意onNewIntent如果沒有調用setIntent(intent),則getIntent()獲取的數據將不是你所期望的。 注意這句話:Note that getIntent() still returns the original Intent. You can use setIntent(Intent) to update it to this new Intent.所以最好是調用setIntent(intent),這樣在使用getIntent()的時候就不會有問題了。