Activity 的啟動模式、應用場景、Intent.FLAG_ 與 taskAffinity
前段時間去面試的時候,有被問到 Activity 的啟動模式。雖然這些東西都瞭解過,但是實際開發中並沒有怎麼應用過。所以被問到應用場景的時候,答的並不好。趁著有空,打算複習鞏固一下。
四種啟動模式與應用場景
standard
活動預設的標準啟動模式。
每當啟動一個新的活動,無論活動的例項是否存在,都會建立一個新的活動的例項,在返回棧中入棧,並處於棧頂的位置。
這個沒什麼好說的,一般的活動都是這個啟動模式。
singleTop
棧頂複用模式。
當要啟動的活動在返回棧中沒有例項,或者有例項但不位於棧頂位置,則與 standard 模式相同,建立一個新的活動例項併入棧;
如果要啟動的活動在返回棧中有例項並且恰好位於棧頂位置,則複用該例項,這個時候,依次執行的方法為:onPause()
onNewIntent(Intent intent)
-> onResume()
。
可以看到 singleTop 和 standard 模式最主要的區別就是 singleTop 模式下,如果活動例項存在並恰好位於棧頂則複用,而 standard 則是無論什麼情況,都會建立新的例項。
使用場景:推送頁,比如優酷的推薦視訊、電商的推薦商品等。
singleTask
棧內複用模式。
啟動活動時,系統首先會檢查返回棧中是否存在該活動的例項,如果存在,則複用該例項。如果該例項處在非棧頂位置,將它上面所有的活動例項出棧,從而位於棧頂位置。這個時候就有兩種情況,分別呼叫的方法也不同,以下執行的方法只針對頁面 A 。
(1)要啟動的活動恰好已經在棧頂,比如 A -> A
onPause()
-> onNewIntent(Intent intent)
-> onResume()
(2)要啟動的活動不在棧頂,比如 A -> B -> A
onNewIntent(Intent intent)
-> onRestart()
-> onStart()
-> onResume()
使用場景:最常見的應該就是 app 的主頁,當我們打開了很多頁面需要返回主頁時,可以利用這種方法。
singleInstance
單例項模式。
如果將一個活動的 launchMode 設定成了 singleInstance,那麼啟動它的時候,系統會單獨給它建立一個新的任務棧,並且該棧只允許這一個活動在其中執行。如果再反覆啟動該活動,不會建立新的任務棧或者例項,但是會呼叫 onPause()
onNewIntent(Intent intent)
-> onResume()
方法。
這裡還有一個需要注意的地方,假如有三個活動 A B C,其中 A B 是預設的啟動模式,C 的啟動模式是 singleInstance,如果當前的頁面啟動順序為 A -> B -> C -> B,那麼按下系統返回鍵,出棧的順序為 B -> B -> A -> C。這是因為 A B B 在一個任務棧中,而 C 在單獨的任務棧中,按照一個棧中先進後出的規則,需要 B B A 全部出棧完畢,才會輪到 C 所在的棧中的元素出棧。
使用場景:一般很少用到,比如說鬧鐘的提示頁、鎖屏頁等。
使用方式
1. 在 AndroidManifest.xml 檔案中配置,比如:
<activity
android:name=".ThirdActivity"
android:launchMode="singleInstance">
</activity>
這是最常用的配置方式。
2. 使用 intent.addFlags
動態設定,比如:
Intent it = new Intent(ThirdActivity.this, SecondActivity.class);
it.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(it);
注意:兩種設定方式還是有區別的。
(1)優先順序:第 2 種方式的優先順序要比第一種高。我曾經遇到過,在清單檔案給活動設定了 singleTop ,但是在快速重複啟動活動的時候,發現還是會開啟兩個頁面,如果遇到了這種情況,下文中有解決辦法。
(2)區別:第 1 種方式只能給活動設定 4 種基本的啟動模式,而第二種方式更加靈活。
Intent.FLAG_
FLAG 不僅能夠設定活動的啟動模式,還能夠改變獲得的執行狀態。關於活動的標誌位大概有 20 多個,這裡只講幾個常用的。
1. FLAG_ACTIVITY_NEW_TASK
Intent it = new Intent(SecondActivity.this, SecondActivity.class);
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(it);
從字面上的意思來看,它會建立一個新的任務棧來存放要啟動的活動例項,但是僅僅單獨設定了這個標誌位的話是沒有任何效果的,並不會建立一個新的任務棧,僅僅只是建立了一個新的活動例項,和預設的啟動模式的表現上完全一致,你們可以自己去試試。
2. FLAG_ACTIVITY_CLEAR_TASK
Intent it = new Intent(SecondActivity.this, SecondActivity.class);
it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(it);
同樣的,從字面上的意思來看,它的作用是清空任務棧,但是僅僅單獨設定了這個標誌位的話,也是沒有任何效果的,棧不會被清空,僅僅只是建立了一個新的活動併入棧。
3. FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK
Intent it = new Intent(SecondActivity.this, SecondActivity.class);
it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(it);
雖然他們兩個單獨使用沒有效果,但是放在一起使用就能看到明顯的效果了。
假設當前的任務棧的 id 是 2,並且我們將要啟動一個新的活動,無論這個活動例項是否已經存在於棧內,都會清空任務棧中的所有元素,建立一個新的活動例項並放到一個新的任務棧中。但是我發現這個新的任務棧的 id 還是 2,不太清楚是直接用了之前的任務棧,還是銷燬了之前的任務棧,恰好新的任務棧的 id “剛好排到了 2 號”。看手機上頁面的切換效果,應該是後者。有了解的同學,希望能科普一下。
適用場景:適用於需要重新登入的場景,可以直接銷燬所有活動然後建立一個新的登入頁。
4. FLAG_ACTIVITY_SINGLE_TOP
Intent it = new Intent(ThirdActivity.this, SecondActivity.class);
it.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(it);
與 singleTop 完全相同,不再多說。
5. FLAG_ACTIVITY_CLEAR_TOP
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
這裡我們分幾種情況討論,以下每次啟動頁面時都添加了 FLAG_ACTIVITY_CLEAR_TOP
:
(1)A -> B
AonPause
-> BonCreate
-> BonStart
-> BonResume
-> AonSaveInstanceState
-> AonStop
可以看到,當要啟動活動在棧中不存在例項時,和預設啟動模式的效果一致。
(2)A -> A
A1onPause
-> A2onCreate
-> A2onStart
-> A2onResume
-> A1onStop
-> A1onDestroy
用 A1 和 A2 方便區分,在這種情況下,在 活動 A 裡面再啟動一個 活動 A ,會建立一個新的活動例項,並且當新的活動啟動完成後,將之前的活動銷燬。
(3)A -> B -> A
從活動 B 要啟動活動 A 開始依次執行的方法:
BonPause
-> A1onDestroy
-> A2onCreate
-> A2onStart
-> A2onResume
-> BonStop
-> BonDestroy
我測試了更多的頁面,比如 A -> B -> C -> D -> E -> B,發現系統建立新的 B 的例項,並且銷燬 B C D E。從而可以總結得出 FLAG_ACTIVITY_CLEAR_TOP
的作用:當要啟動的活動例項不存在時,建立新的活動例項入棧;當要啟動的活動例項已經存在時,建立新的活動例項入棧的同時,會把已存在的活動例項以及棧中它上面所有的活動都銷燬掉。
下面是我在開發的時候遇到的一個問題,大家可以參考一下:
<activity
android:name=".ThirdActivity"
android:launchMode="singleTop">
</activity>
在清單檔案中給活動設定了 singleTop 的模式,但是實際啟動的時候,由於某些原因非常快速的啟動了兩次,結果發現居然是啟動了兩個活動出現了兩個頁面,完全不是想要的結果。那麼如果保證只出現一個頁面呢?
不要再清單檔案設定啟動模式了,動態設定如下:
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
如果 SINGLE_TOP 生效,那麼就會複用例項執行 onNewIntent
方法;如果 SINGLE_TOP 沒有生效,那麼會再建立一個新的頁面執行 onCreate
方法,在 CLEAR_TOP 的作用下,會將第一個頁面銷燬,從而保證了只有一個頁面顯示給使用者。所以,要同時在 onNewIntent
和 onCreate
方法裡面做處理。
taskAffinity
<activity
...
android:taskAffinity=".taskname">
</activity>
taskAffinity 的屬性是設定活動和棧的依附關係。一般情況下,一個 app 的所有活動都在一個棧中,但是我們也可以通過設定 singleInstance 來建立新的任務棧。利用 taskAffinity 屬性也可以實現這一功能。
每個活動都可以設定自己的 taskAffinity 屬性,這個屬性指出了它希望進入的棧。如果一個活動沒有指明該活動的 taskAffinity 屬性,那麼它的這個屬性就等於 Application 指明的 taskAffinity,如果 Application 也沒有指明,那麼該 taskAffinity 的值就等於包名。而棧也有自己的 affinity 屬性,它的值等於它的根活動的 taskAffinity的值。所以,設定該屬性的時候,要按照包名的格式來設定,用 . 號分隔。
下面我們通過幾個具體的例子來看看它的效果。
1.我們在清單檔案裡面給 B 設定如下:
<activity
android:name=".BActivity"
android:launchMode="singleTask"
android:taskAffinity=".second"
</activity>
(1)A -> B -> B
由 A 到 B 時,由於 B 的例項還不存在,會建立一個新的任務棧和 B 的例項,然後再由 B 到 B,由於 B 已經存在了,這個時候會執行 B 的 onPause()
-> onNewIntent(Intent intent)
-> onResume()
方法。
(2)A -> B -> C -> A
這種情況下,我們也是隻給 B 設定了啟動模式, A1 C A2 都是未進行任何設定的。這個時候會發現,由 A 到 B 時建立了一個新的任務棧,之後的 C 和 A2 都和 B 在一個任務棧中,A2 和 A1 是不同棧中的兩個活動例項。
(3)A -> B -> C -> B
和上面的設定相同,由 A 到 B 時建立了一個新的任務棧,但是當 C 再到 B 時會發生什麼呢?C 會被銷燬,然後 B1 會執行 onNewIntent
-> onRestart
-> onStart
-> onResume
方法。
2.下面的幾種情況的設定方法和上面不同了,我們在清單檔案裡面只設置了 taskAffinity
而沒有設定 launchMode
。
<activity
android:name=".SecondActivity"
android:taskAffinity=".second">
</activity>
(1)A -> B -> C -> B -> A
除了 taskAffinity
什麼都沒設定,這個時候沒有任何效果,只會不停地建立例項然後入棧。
(2)A -> B -> C
在 A -> B 的時候,動態設定了
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
A -> B 建立一個新的任務棧,並且之後的 C 和 B 在一個棧中。和上面個的第(2)情況是一致的。
(3)A -> B -> B
在 A -> B 和 B -> B 的時候,都設定了
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
A -> B 建立一個新的任務棧,B -> B 的時候,不會建立新的任務棧,並且不會建立新的 B 的例項,也不會執行 B 的任何方法!這點需要注意了,和上面的第(1)種情況並不一樣。
後記
在寫這篇部落格的時候,我發現啟動模式原來有這麼多的東西。我這裡也只是寫了一部分,更深層次的東西也沒有研究到,但寫了上面的這些感覺收穫也蠻大的。如果你們有興趣可以自己去研究研究。