啟動模式

在了解啟動模式之前,要先了解 Android 的 activity 管理方式

google 官網(wǎng)定義的 五種啟動模式

簡單來說:

  1. Android 中可以開啟多個 App,App 中可以包含多個 Activity,這些 Activity 是用任務(wù)棧的方式進行管理的。

  2. 任務(wù)棧是與用戶交互的頁面順序,但與 App 不是 一對一的關(guān)系,Android 根據(jù) Activity 的 launchMode 和 taskAffinity 等屬性,管理 Acticity 的棧歸屬

  3. 任務(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 主頁的需求為:

  1. App 的第一個頁面

  2. 后續(xù)頁面不需要啟動新的主頁,只需回退到主頁

這意味著:

  1. 該 Activity 處于棧底

  2. 棧中只會存在一個該 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ū)別在于:

  1. singleTask 可以處在棧中的任意位置,而 singleInstancePerTask 只能處于棧底

  2. singleTask 在 Android 中,只會有一個實例(startActivity 時,沒有則創(chuàng)建,有則調(diào)起所在的棧,并回退到該 activity,同時回調(diào) onNewIntent ),而 singleInstancePerTask 可以搭配 flag,在多個棧中有不同實例(可以有兩個棧同時以該 Activity 為棧底)

  3. 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。

  1. 新建一個 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>
  1. 在設(shè)備中,運行 APP A。

  2. 新建 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) })
}
  1. 打開任務(wù)管理器,會發(fā)現(xiàn)在 APP B 的任務(wù)堆棧中,存在 APP A 的 Activity:
launch_mode_1.png

因而,暴露出去給其他 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ù)記錄,切換到桌面才會

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容