在了解啟動模式之前,要先了解 Android 的 activity 管理方式
google 官網(wǎng)定義的 五種啟動模式
簡單來說:
Android 中可以開啟多個 App,App 中可以包含多個 Activity,這些 Activity 是用任務(wù)棧的方式進行管理的。
任務(wù)棧是與用戶交互的頁面順序,但與 App 不是 一對一的關(guān)系,Android 根據(jù) Activity 的 launchMode 和 taskAffinity 等屬性,管理 Acticity 的棧歸屬
任務(wù)棧內(nèi) Activity 的順序不可更改,遵循入棧彈棧(先進后出)的管理原則
頁面說明:
LaunchActivity:manifest 中設(shè)置了category launch 的啟動頁面
SplashActicity:啟動時的閃屏頁面
MainActivity: app 主頁
啟動模式 Standard
默認(rèn)的啟動模式
Standard 啟動模式是 App 默認(rèn)的啟動模式。在 AndroidManifest.xml 文件中,不對 activity 的 launchMode 參數(shù)進行設(shè)置,這個 activity 就會以 standard 模式啟動。
在 Android 中,點擊桌面圖標(biāo),首次啟動 App,創(chuàng)建應(yīng)用的 LaunchActivity A,然后點擊一個按鈕,跳到另一個 Activity B,這時候,Activity A 的狀態(tài)會被保留,且壓入任務(wù)棧中,Activity B 會被創(chuàng)建,并且顯示在任務(wù)棧頂。
如果 app 中有 activity A、B、C,且都是以 standard 模式啟動,那么,多次頁面跳轉(zhuǎn)后,它的堆??赡茏兂桑?/p>
A - B - C - B - B - C - C - A - C
只要開啟新 Activity 就會創(chuàng)建,進棧。然后觸發(fā)頁面返回操作時,按順序一一回退頁面。
- NOTE:在 Standard 模式中,Activity 可以存在多個實例,加載到任意任務(wù)棧的任意位置中
singleTop 啟動模式 —— 避免同個頁面嵌套地獄
在 App 中,詳情頁中可能存在其他物品的詳情頁鏈接,用戶大概率會點擊進入下一個詳情鏈接,然后詳情鏈接內(nèi)又有詳情鏈接,當(dāng)瀏覽了十來個物品后,想回到最初的列表頁,需要瘋狂點擊返回按鈕。避免這種情況,則可以使用 singleTop 的啟動模式 —— 當(dāng)棧頂已經(jīng)是詳情頁時,再次打開一個詳情頁,不會重新創(chuàng)建頁面,只會回調(diào)當(dāng)前頁面的 onPause ->onNewIntent 傳遞新的 Intent -> onResume
class SingleTopActivity : AppCompatActivity() {
private val idKey = "id"
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_launch)
initViews()
updateIntent()
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// 更新 intent
setIntent(intent)
// 重新設(shè)置頁面內(nèi)容
updateIntent()
}
private fun initViews() {
textView = findViewById(R.id.tv_title)
textView.setOnClickListener {
startActivity(Intent(this, SingleTopActivity::class.java).apply {
putExtra(idKey, intent.getIntExtra(idKey, 0) + 1)
})
}
}
private fun updateIntent() {
val id = intent.getIntExtra(idKey, 0)
textView.setText("get id: $id")
}
}
不斷點擊打開多個 SingleTopActivity 會發(fā)現(xiàn)頁面中顯示的 id:0 在一直增加,打開多個后,點擊返回,即可回到上一個頁面,而不是在 SingleTopActivity 中一直循環(huán)
但實際功能中,經(jīng)常是幾個頁面嵌套的循環(huán)使用,如微信中 聊天頁面 -> 個人信息頁面 -> (發(fā)送消息)聊天頁面。早期的時候,這幾個頁面可以一直循環(huán),返回時需要多次點擊返回按鈕,但現(xiàn)在聊天頁面直接返回的到了首頁。處理多個頁面嵌套的方式,可以使用下文返回主頁(循環(huán)入口)方式處理
如果 app 中有 activity A、B、C,其中 B 的模式啟動為 singleTop, A、C 為 standard,那么,頁面跳轉(zhuǎn)情況如下:
啟動 A: A
啟動 B: A - B
啟動 C: A - B - C
啟動 C: A - B - C - C
啟動 B: A - B - C -C - B
啟動 B: A - B - C -C - B*
(* 表示沒有重新創(chuàng)建頁面實例,但調(diào)用了 newIntent 進行更新)
- NOTE:只有當(dāng) singleTop 模式的 Activity 存在于棧頂時,Activity 的表現(xiàn)與 standard 不同
啟動模式 singleInstancePerTask
最適合作為 MainActivity 的啟動模式
一般,App 主頁的需求為:
App 的第一個頁面
后續(xù)頁面不需要啟動新的主頁,只需回退到主頁
這意味著:
該 Activity 處于棧底
棧中只會存在一個該 Activity 實例
因而 singleInstancePerTask 最適合作為 MainActivity 的啟動模式
在 standard 和 singleTop 的啟動模式下,activity 可以存在多個實例,位于棧中的任何地方。但 singleInstancePerTask 在一個棧中,只會有一個實例。即:
如果 app 中有 activity A、B 、C,其中 B 的模式啟動為 singleInstancePerTask,A、C 為 standard,那么,頁面跳轉(zhuǎn)情況如下:
啟動 A: A
啟動 B: A -> 切換棧 -> B
啟動 C: A | B - C
啟動 A: A | B - C- A
啟動 B: A | B*
返回:A
在實際應(yīng)用中,A - SplashActivity、LaunchActivity; B - MainActivity;C - 普通頁面
如果 沒有主動將 Activity A finish ,在 Activity B 中返回上一個頁面,會切換回 Activity A;或者在處于 Activity B 任務(wù)棧時,回到桌面,系統(tǒng)會自動關(guān)閉 Activity A 所在的隱藏棧。
- NOTE:在 Android 12 以下的機器,設(shè)置后無效,等同于 standard 模式
啟動模式 SingleTask
singleTask 與 singleInstancePerTask 的區(qū)別
在沒有 singleInstancePerTask 模式之前,singleTask 是 MainActivity 啟動模式首選,二者的區(qū)別在于:
singleTask 可以處在棧中的任意位置,而 singleInstancePerTask 只能處于棧底
singleTask 在 Android 中,只會有一個實例(startActivity 時,沒有則創(chuàng)建,有則調(diào)起所在的棧,并回退到該 activity,同時回調(diào) onNewIntent ),而 singleInstancePerTask 可以搭配 flag,在多個棧中有不同實例(可以有兩個棧同時以該 Activity 為棧底)
singleInstancePerTask 會在系統(tǒng)已存在沒有該實例的同 taskAffinity 任務(wù)棧時,重新開啟一個棧,而 singleTask 則會直接在該棧頂創(chuàng)建 Activity
如果 app 中有 activity A、B 、C,其中 B 的模式啟動為 singleTask,A、C 為 standard,那么,頁面跳轉(zhuǎn)情況如下:
啟動 A: A
啟動 B: A - B
啟動 C: A - B - C
啟動 A: A - B - C- A
啟動 B: A - B*
A - SplashActivity、LaunchActivity; B - MainActivity;C - 普通頁面。在 Android 12 之前的實際應(yīng)用中,需要通過代碼來控制 Activity A 的自動跳轉(zhuǎn)和關(guān)閉:
SplashActicity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 判斷是否處于棧底(首次啟動)
if (isTaskRoot) {
// 跳轉(zhuǎn)到真正的啟動頁面
startActivity(Intent(this, MainActivity::class.java))
}
finish()
}
從桌面點擊 icon 喚醒后臺 App 時,如果 MainActivity 為 singleTask 模式,會創(chuàng)建 SplashActivity,置于原本棧頂,然后回調(diào)到 onCreate 中,finish SplashActivity,顯示原本任務(wù)棧。而如果 MainActivity 為 singleInstancePerTask 啟動模式,則不會有創(chuàng)建 SplashActivity 的流程,直接將已存在的 MainActivity 為根的任務(wù)棧喚醒到前臺。
- NOTE:當(dāng)將 MainActivty 作為 LanchActivity ,且啟動模式為 SingleTask 時,可能存在 bug,詳見下文 啟動其他App
singleInstance 啟動模式
singleInstance 在 Android 中,也不會存在第二個實例,且 singleInstance 的棧中,有且只有這一個 Activity(不會繼續(xù)疊加新的)
如果 app 中有 activity A、B 、C,其中 B 的模式啟動為 singleTask,A、C 為 standard,那么,頁面跳轉(zhuǎn)情況如下:
啟動 A: A
啟動 B: A -> 切換棧 -> B(前臺棧)
返回:B(onDestroy 銷毀) -> 切換棧 -> A
啟動 B: A -> 切換棧 -> B(前臺棧)
啟動 C: B (后臺棧)| A - C(前臺棧,顯示 C)
返回 : B (后臺棧) | A(前臺棧,顯示 A)
返回 : B(顯示 B)
當(dāng)啟動 singleInstance 的 Activity 時,會切換到新的任務(wù)棧,在 singleInstance 中啟動 Activity 時,如果原本任務(wù)棧還存在,會回到原本任務(wù)棧,否則啟動新任務(wù)棧;當(dāng)回到原本任務(wù)棧時,原本的任務(wù)棧會變成前臺任務(wù)棧,只有前臺任務(wù)棧全部退出時,才會顯示 singleInstance 所在的任務(wù)棧(即 singleInstance 的 Activity 會在最后顯示)
如果此間插入 回桌面 的操作,那么兩個任務(wù)棧的聯(lián)系會被取消,singleInstance 的任務(wù)棧處于后臺狀態(tài),不會再次返回前臺,下次啟動該 Activity 時,沒有創(chuàng)建新的 Activity,只會回調(diào) onNewIntent(如果后臺任務(wù)棧沒有被系統(tǒng)銷毀)
啟動 A: A
啟動 B: A -> 切換棧 -> B(前臺棧)
啟動 C: B -> 切換棧 -> (A - )C(前臺棧)
回桌面:B(后臺棧);A - C (后臺棧)
點擊桌面圖標(biāo):A - C
返回 : A
返回 : 桌面
點擊桌面圖標(biāo),啟動 A:A
啟動 B: A -> 切換棧 -> B*(前臺棧,調(diào)用 onNewIntent)
App 間的相互跳轉(zhuǎn)
Android 的 App 啟動方式
Android 的桌面(Launcher),實質(zhì)上是一個 App。點擊桌面圖標(biāo)啟動 App,就是從一個 App 跳轉(zhuǎn)到另一個 App 的過程
當(dāng)點擊 launcher 中的圖標(biāo)時,會調(diào)用 startActivity 啟動對應(yīng) APP 的 AndroidManifest 中注冊的 LaunchActivity,且設(shè)置了 flag: Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS (api 28)。不同android 版本設(shè)置的 flag 不同,但本質(zhì)上,都是啟動對應(yīng) App 的 LaunchActivity,或者將 LaunchActivity 所在的任務(wù)棧調(diào)回前臺。
此時,如果 LaunchActivity 的 launchMode 被設(shè)置為 singleTask,在 LaunchActivity 所在任務(wù)?;卣{(diào)到前臺的同時,任務(wù)棧會回退到 LaunchActivity,并回調(diào) onNewIntent。即,每次從桌面點擊圖標(biāo)回到 App 頁面,都相當(dāng)于重啟 App。因而,不建議將 MainActivity 作為 LaunchActivity 的同時,還將其 launchMode 設(shè)置為 singleTask(或者singleInstancePerTask) ,而是通過添加一個 SplashActivity 作為 LaunchActivity 的方式,區(qū)分 App 啟動頁和 App 主頁。
處于啟動優(yōu)化考慮,讓用戶無感知跳轉(zhuǎn)啟動頁面,可將 SplashActivity 和 MainActivity 的 window 背景設(shè)置為相同的啟動頁。
如果一定要將 MainActivity 設(shè)置為 LaunchActivity,請移除 singleTask 的 launchMode 設(shè)置,并通過跳轉(zhuǎn)時設(shè)置 intentFlag,來實現(xiàn)回到首頁功能。
val startMain = Intent(this, MainActivity::class.java)
startMain.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(startMain)
- NOTE: 如果將 MainActivity 設(shè)置為 LaunchActivity,launchMode 為 standard,在正常系統(tǒng)調(diào)用時,沒有問題。但如果其他應(yīng)用,只是簡單地 將暴露出去的 launchActivity 作為 intent 啟動了,那么 App 的所有 Activity,都會被加載入調(diào)起此 App 的原始 App 的堆棧,如下文演示:
任務(wù)棧 和 App 不是一一對應(yīng)的關(guān)系
- 一個 App,可以有多個任務(wù)棧
上文提到,只有 singleInstance 會啟動獨立任務(wù)棧。這個任務(wù)棧,在任務(wù)管理器中,是隱形狀態(tài),但在任務(wù)管理器中看不到 task,不意味著不存在。這個隱形棧內(nèi)的 Activity,如果被用戶切出之后(開啟新的 Activity),除了完全退出當(dāng)前任務(wù)棧,無法返回;且如果回到桌面在通過任務(wù)?;蛘咦烂鎴D標(biāo)調(diào)起應(yīng)用,完全無法回到該 Activity。但在內(nèi)存不吃緊時,這個隱形棧依舊存在于內(nèi)存中,下次啟用該 Activity 時,直接回調(diào) onNewIntent。
任務(wù)管理器顯示 task,主要根據(jù)是 AndroidManifest 中一個重要屬性 taskAffinity 。沒有明確設(shè)置時,這個值默認(rèn)為包名。所以當(dāng)各種會開啟新任務(wù)棧的 launchMode 被設(shè)置后,而 taskAffinity 又沖突了,那么,處于后臺的 task 會被隱藏。
設(shè)置了 taskAffinity 后,可以對 App 的不同 Activity 進行 task 分組。但是,只有會發(fā)生 任務(wù)棧切換的 task,此配置才有效,即,standard 模式的 activity,設(shè)置了 taskAffinity 之后,依舊只會在當(dāng)前 task 疊加和刪除。
如果 singleInstance 啟動模式的 taskAffinity 和其他 taskAffinity 設(shè)置為一樣的,應(yīng)用行為與沒有設(shè)置時一致,即一樣開啟新的任務(wù)棧啟動 singleInstance acitivity。
- 一個任務(wù)棧內(nèi),可以顯示多 App 的 Activity
前面說到 standard 模式下,不會發(fā)生切棧效果,即便是開啟了其他 app 的 Activity。
- 新建一個 APP A, manifest 中配置:
<!-- exported 必須為 true-->
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<!-- 配置 action 為 view, category 為 DEFAULT, 將 activity 暴露給其他 APP 調(diào)用 -->
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
在設(shè)備中,運行 APP A。
新建 APP B,啟用上方定義的 MainActivity:
findViewById<Button>(R.id.btn_action).setOnClickListener {
// 會彈出設(shè)備中所有暴露出了 action 為 view 的 Activity 的 應(yīng)用列表,選中之前的 APP A
// 也可通過添加 scheme 的方式指定到當(dāng)前 app,可自行了解
val intent = Intent(Intent.ACTION_VIEW)
startActivity(intent)
// 也可通過 componentName 直接指定打開的 activity,但 activity 不存在的話會崩潰
// val componentName = ComponentName("app.package.name", "app.package.name.ActivityA")
// startActivity(Intent().apply { setComponent(componentName) })
}
- 打開任務(wù)管理器,會發(fā)現(xiàn)在 APP B 的任務(wù)堆棧中,存在 APP A 的 Activity:

因而,暴露出去給其他 App 喚醒的 Activity,如可能被推廣頁調(diào)起的詳情,或者啟動頁面,如果不想被加載進其他 App 的棧,需 將暴露出去,可能被其他 App 調(diào)起的 Activity 設(shè)置為 singleTask 啟動模式(固定在自身 app 任務(wù)棧上顯示)。
如果需要 啟動其他 App 暴露的 Activity,且不希望將 Activity 加載在自身棧內(nèi),在 intent 中添加 flag:FLAG_ACTIVITY_NEW_TASK
總結(jié)
standard:創(chuàng)建并在堆棧棧頂加入 Activity
-
singleTop:
基本與 standard 一致。
除非是在棧頂再次啟動 —— 直接回調(diào) activity 的 onPause - onNewInstent - onResume
-
singleTask:
切換到對應(yīng) taskAffinity 中顯示。
如果不存在 taskAffinity 一致的 task,創(chuàng)建 task,創(chuàng)建 Activity,入棧;
如果已存在,將對應(yīng) task 提到前臺;
如果已存在的 task 內(nèi),已存在該 Activity,將 task 回退到該 Activity 位置,并回調(diào) Activity 的 onNewIntent 方法
-
singleInstancePerTask:
切換到對應(yīng) taskAffinity 中顯示。
如果不存在 taskAffinity 一致的 task,創(chuàng)建 task,創(chuàng)建 Activity,入棧;
如果已存在,且該 Activity 即為棧底 Activity,將對應(yīng) task 提到前臺,且清空棧頂所有 Activity,回調(diào) onNewIntent,并顯示;
-
如果已存在 taskAffinity 一致的 task,且 task 中沒有該 activity,創(chuàng)建一個同 taskAffinity 的任務(wù)棧,創(chuàng)建 Activity,入棧。
原本被隱藏的 task,會在返回時重新提回前臺;
又或是在點擊home 鍵回到桌面被銷毀
-
singleInstance:
切換到對應(yīng) taskAffinity 中顯示。
且 task 內(nèi)只會有這一個 Activity
如果已存在該 Activity 的單獨任務(wù)棧,調(diào)起任務(wù)棧,回調(diào) onNewIntent,并顯示;
若存在同名任務(wù)棧,非該 Activity 單獨任務(wù)棧,創(chuàng)建新的任務(wù)棧,并置于前臺,原本任務(wù)棧隱藏
-
多個同 taskAffinity 棧的處理
在任務(wù)管理器中,只會顯示最近出現(xiàn)在前臺的一個,其他的全部被隱藏
只有 singleInstance 和 singleInstancePerTask 在已存在同 taskAffinity 任務(wù)棧的情況下,會開啟新的任務(wù)棧,并將原本任務(wù)棧隱藏;
回到桌面后,隱藏棧自動被銷毀 (Android 12)
-
點擊桌面圖標(biāo)啟動 Activity
不存在棧,創(chuàng)建棧,創(chuàng)建 launchActivity,入棧,顯示
存在棧,且 LaunchActivity 為 standard,調(diào)起任務(wù)棧
存在棧, LaunchActivity 為 singleTop,且當(dāng)前頁面為 LaunchActivity,回調(diào) onNewIntent
存在棧, LaunchActivity 為其他需要切換棧的 launchMode,在任務(wù)棧頂啟動 LaunchActivity
Android 12,存在棧,且棧底為 singleInstancePerTask,調(diào)起任務(wù)棧
Android 12 中,任務(wù)棧切換 task,不會銷毀原本任務(wù)記錄,切換到桌面才會