启动模式

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

google 官网定义的 五种启动模式

简单来说:

  1. Android 中可以开启多个 App,App 中可以包含多个 Activity,这些 Activity 是用任务栈的方式进行管理的。

  2. 任务栈是与用户交互的页面顺序,但与 App 不是 一对一的关系,Android 根据 Activity 的 launchMode 和 taskAffinity 等属性,管理 Acticity 的栈归属

  3. 任务栈内 Activity 的顺序不可更改,遵循入栈弹栈(先进后出)的管理原则

页面说明:

  • LaunchActivity:manifest 中设置了category launch 的启动页面

  • SplashActicity:启动时的闪屏页面

  • MainActivity: app 主页

启动模式 Standard

默认的启动模式

Standard 启动模式是 App 默认的启动模式。在 AndroidManifest.xml 文件中,不对 activity 的 launchMode 参数进行设置,这个 activity 就会以 standard 模式启动。

在 Android 中,点击桌面图标,首次启动 App,创建应用的 LaunchActivity A,然后点击一个按钮,跳到另一个 Activity B,这时候,Activity A 的状态会被保留,且压入任务栈中,Activity B 会被创建,并且显示在任务栈顶。

如果 app 中有 activity A、B、C,且都是以 standard 模式启动,那么,多次页面跳转后,它的堆栈可能变成:

A - B - C - B - B - C - C - A - C

只要开启新 Activity 就会创建,进栈。然后触发页面返回操作时,按顺序一一回退页面。

  • NOTE:在 Standard 模式中,Activity 可以存在多个实例,加载到任意任务栈的任意位置中

singleTop 启动模式 —— 避免同个页面嵌套地狱

在 App 中,详情页中可能存在其他物品的详情页链接,用户大概率会点击进入下一个详情链接,然后详情链接内又有详情链接,当浏览了十来个物品后,想回到最初的列表页,需要疯狂点击返回按钮。避免这种情况,则可以使用 singleTop 的启动模式 —— 当栈顶已经是详情页时,再次打开一个详情页,不会重新创建页面,只会回调当前页面的 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)
        // 重新设置页面内容
        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 会发现页面中显示的 id:0 在一直增加,打开多个后,点击返回,即可回到上一个页面,而不是在 SingleTopActivity 中一直循环

但实际功能中,经常是几个页面嵌套的循环使用,如微信中 聊天页面 -> 个人信息页面 -> (发送消息)聊天页面。早期的时候,这几个页面可以一直循环,返回时需要多次点击返回按钮,但现在聊天页面直接返回的到了首页。处理多个页面嵌套的方式,可以使用下文返回主页(循环入口)方式处理

如果 app 中有 activity A、B、C,其中 B 的模式启动为 singleTop, A、C 为 standard,那么,页面跳转情况如下:

启动 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*
(* 表示没有重新创建页面实例,但调用了 newIntent 进行更新)

  • NOTE:只有当 singleTop 模式的 Activity 存在于栈顶时,Activity 的表现与 standard 不同

启动模式 singleInstancePerTask

最适合作为 MainActivity 的启动模式

一般,App 主页的需求为:

  1. App 的第一个页面

  2. 后续页面不需要启动新的主页,只需回退到主页

这意味着:

  1. 该 Activity 处于栈底

  2. 栈中只会存在一个该 Activity 实例

因而 singleInstancePerTask 最适合作为 MainActivity 的启动模式

在 standard 和 singleTop 的启动模式下,activity 可以存在多个实例,位于栈中的任何地方。但 singleInstancePerTask 在一个栈中,只会有一个实例。即:

如果 app 中有 activity A、B 、C,其中 B 的模式启动为 singleInstancePerTask,A、C 为 standard,那么,页面跳转情况如下:

启动 A: A
启动 B: A -> 切换栈 -> B
启动 C: A | B - C
启动 A: A | B - C- A
启动 B: A | B*
返回:A

在实际应用中,A - SplashActivity、LaunchActivity; B - MainActivity;C - 普通页面

如果 没有主动将 Activity A finish ,在 Activity B 中返回上一个页面,会切换回 Activity A;或者在处于 Activity B 任务栈时,回到桌面,系统会自动关闭 Activity A 所在的隐藏栈。

  • NOTE:在 Android 12 以下的机器,设置后无效,等同于 standard 模式

启动模式 SingleTask

singleTask 与 singleInstancePerTask 的区别

在没有 singleInstancePerTask 模式之前,singleTask 是 MainActivity 启动模式首选,二者的区别在于:

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

  2. singleTask 在 Android 中,只会有一个实例(startActivity 时,没有则创建,有则调起所在的栈,并回退到该 activity,同时回调 onNewIntent ),而 singleInstancePerTask 可以搭配 flag,在多个栈中有不同实例(可以有两个栈同时以该 Activity 为栈底)

  3. singleInstancePerTask 会在系统已存在没有该实例的同 taskAffinity 任务栈时,重新开启一个栈,而 singleTask 则会直接在该栈顶创建 Activity

如果 app 中有 activity A、B 、C,其中 B 的模式启动为 singleTask,A、C 为 standard,那么,页面跳转情况如下:

启动 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 之前的实际应用中,需要通过代码来控制 Activity A 的自动跳转和关闭:

SplashActicity

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 判断是否处于栈底(首次启动)
    if (isTaskRoot) {
        // 跳转到真正的启动页面
        startActivity(Intent(this, MainActivity::class.java))
    }
    
    finish()
}

从桌面点击 icon 唤醒后台 App 时,如果 MainActivity 为 singleTask 模式,会创建 SplashActivity,置于原本栈顶,然后回调到 onCreate 中,finish SplashActivity,显示原本任务栈。而如果 MainActivity 为 singleInstancePerTask 启动模式,则不会有创建 SplashActivity 的流程,直接将已存在的 MainActivity 为根的任务栈唤醒到前台。

  • NOTE:当将 MainActivty 作为 LanchActivity ,且启动模式为 SingleTask 时,可能存在 bug,详见下文 启动其他App

singleInstance 启动模式

singleInstance 在 Android 中,也不会存在第二个实例,且 singleInstance 的栈中,有且只有这一个 Activity(不会继续叠加新的)

如果 app 中有 activity A、B 、C,其中 B 的模式启动为 singleTask,A、C 为 standard,那么,页面跳转情况如下:

启动 A: A
启动 B: A -> 切换栈 -> B(前台栈)
返回:B(onDestroy 销毁) -> 切换栈 -> A
启动 B: A -> 切换栈 -> B(前台栈)
启动 C: B (后台栈)| A - C(前台栈,显示 C)
返回 : B (后台栈) | A(前台栈,显示 A)
返回 : B(显示 B)

当启动 singleInstance 的 Activity 时,会切换到新的任务栈,在 singleInstance 中启动 Activity 时,如果原本任务栈还存在,会回到原本任务栈,否则启动新任务栈;当回到原本任务栈时,原本的任务栈会变成前台任务栈,只有前台任务栈全部退出时,才会显示 singleInstance 所在的任务栈(即 singleInstance 的 Activity 会在最后显示)

如果此间插入 回桌面 的操作,那么两个任务栈的联系会被取消,singleInstance 的任务栈处于后台状态,不会再次返回前台,下次启动该 Activity 时,没有创建新的 Activity,只会回调 onNewIntent(如果后台任务栈没有被系统销毁)

启动 A: A
启动 B: A -> 切换栈 -> B(前台栈)
启动 C: B -> 切换栈 -> (A - )C(前台栈)
回桌面:B(后台栈);A - C (后台栈)
点击桌面图标:A - C
返回 : A
返回 : 桌面
点击桌面图标,启动 A:A
启动 B: A -> 切换栈 -> B*(前台栈,调用 onNewIntent)

App 间的相互跳转

Android 的 App 启动方式

Android 的桌面(Launcher),实质上是一个 App。点击桌面图标启动 App,就是从一个 App 跳转到另一个 App 的过程

当点击 launcher 中的图标时,会调用 startActivity 启动对应 APP 的 AndroidManifest 中注册的 LaunchActivity,且设置了 flag: Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS (api 28)。不同android 版本设置的 flag 不同,但本质上,都是启动对应 App 的 LaunchActivity,或者将 LaunchActivity 所在的任务栈调回前台。

此时,如果 LaunchActivity 的 launchMode 被设置为 singleTask,在 LaunchActivity 所在任务栈回调到前台的同时,任务栈会回退到 LaunchActivity,并回调 onNewIntent。即,每次从桌面点击图标回到 App 页面,都相当于重启 App。因而,不建议将 MainActivity 作为 LaunchActivity 的同时,还将其 launchMode 设置为 singleTask(或者singleInstancePerTask) ,而是通过添加一个 SplashActivity 作为 LaunchActivity 的方式,区分 App 启动页和 App 主页。

处于启动优化考虑,让用户无感知跳转启动页面,可将 SplashActivity 和 MainActivity 的 window 背景设置为相同的启动页。

如果一定要将 MainActivity 设置为 LaunchActivity,请移除 singleTask 的 launchMode 设置,并通过跳转时设置 intentFlag,来实现回到首页功能。

val startMain = Intent(this, MainActivity::class.java)
startMain.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(startMain)
  • NOTE: 如果将 MainActivity 设置为 LaunchActivity,launchMode 为 standard,在正常系统调用时,没有问题。但如果其他应用,只是简单地 将暴露出去的 launchActivity 作为 intent 启动了,那么 App 的所有 Activity,都会被加载入调起此 App 的原始 App 的堆栈,如下文演示:

任务栈 和 App 不是一一对应的关系

  • 一个 App,可以有多个任务栈

上文提到,只有 singleInstance 会启动独立任务栈。这个任务栈,在任务管理器中,是隐形状态,但在任务管理器中看不到 task,不意味着不存在。这个隐形栈内的 Activity,如果被用户切出之后(开启新的 Activity),除了完全退出当前任务栈,无法返回;且如果回到桌面在通过任务栈或者桌面图标调起应用,完全无法回到该 Activity。但在内存不吃紧时,这个隐形栈依旧存在于内存中,下次启用该 Activity 时,直接回调 onNewIntent。

任务管理器显示 task,主要根据是 AndroidManifest 中一个重要属性 taskAffinity 。没有明确设置时,这个值默认为包名。所以当各种会开启新任务栈的 launchMode 被设置后,而 taskAffinity 又冲突了,那么,处于后台的 task 会被隐藏。

设置了 taskAffinity 后,可以对 App 的不同 Activity 进行 task 分组。但是,只有会发生 任务栈切换的 task,此配置才有效,即,standard 模式的 activity,设置了 taskAffinity 之后,依旧只会在当前 task 叠加和删除。

如果 singleInstance 启动模式的 taskAffinity 和其他 taskAffinity 设置为一样的,应用行为与没有设置时一致,即一样开启新的任务栈启动 singleInstance acitivity。

  • 一个任务栈内,可以显示多 App 的 Activity

前面说到 standard 模式下,不会发生切栈效果,即便是开启了其他 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 调用 -->
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>
  1. 在设备中,运行 APP A。

  2. 新建 APP B,启用上方定义的 MainActivity:

findViewById<Button>(R.id.btn_action).setOnClickListener {
    // 会弹出设备中所有暴露出了 action 为 view 的 Activity 的 应用列表,选中之前的 APP A
    // 也可通过添加 scheme 的方式指定到当前 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. 打开任务管理器,会发现在 APP B 的任务堆栈中,存在 APP A 的 Activity:
launch_mode_1.png

因而,暴露出去给其他 App 唤醒的 Activity,如可能被推广页调起的详情,或者启动页面,如果不想被加载进其他 App 的栈,需 将暴露出去,可能被其他 App 调起的 Activity 设置为 singleTask 启动模式(固定在自身 app 任务栈上显示)

如果需要 启动其他 App 暴露的 Activity,且不希望将 Activity 加载在自身栈内,在 intent 中添加 flag:FLAG_ACTIVITY_NEW_TASK

总结

  • standard:创建并在堆栈栈顶加入 Activity

  • singleTop:

    • 基本与 standard 一致。

    • 除非是在栈顶再次启动 —— 直接回调 activity 的 onPause - onNewInstent - onResume

  • singleTask:

    • 切换到对应 taskAffinity 中显示。

    • 如果不存在 taskAffinity 一致的 task,创建 task,创建 Activity,入栈;

    • 如果已存在,将对应 task 提到前台;

    • 如果已存在的 task 内,已存在该 Activity,将 task 回退到该 Activity 位置,并回调 Activity 的 onNewIntent 方法

  • singleInstancePerTask:

    • 切换到对应 taskAffinity 中显示。

    • 如果不存在 taskAffinity 一致的 task,创建 task,创建 Activity,入栈;

    • 如果已存在,且该 Activity 即为栈底 Activity,将对应 task 提到前台,且清空栈顶所有 Activity,回调 onNewIntent,并显示;

    • 如果已存在 taskAffinity 一致的 task,且 task 中没有该 activity,创建一个同 taskAffinity 的任务栈,创建 Activity,入栈。

      • 原本被隐藏的 task,会在返回时重新提回前台;

      • 又或是在点击home 键回到桌面被销毁

  • singleInstance:

    • 切换到对应 taskAffinity 中显示。

    • 且 task 内只会有这一个 Activity

    • 如果已存在该 Activity 的单独任务栈,调起任务栈,回调 onNewIntent,并显示;

    • 若存在同名任务栈,非该 Activity 单独任务栈,创建新的任务栈,并置于前台,原本任务栈隐藏

  • 多个同 taskAffinity 栈的处理

    • 在任务管理器中,只会显示最近出现在前台的一个,其他的全部被隐藏

    • 只有 singleInstance 和 singleInstancePerTask 在已存在同 taskAffinity 任务栈的情况下,会开启新的任务栈,并将原本任务栈隐藏;

    • 回到桌面后,隐藏栈自动被销毁 (Android 12)

  • 点击桌面图标启动 Activity

    • 不存在栈,创建栈,创建 launchActivity,入栈,显示

    • 存在栈,且 LaunchActivity 为 standard,调起任务栈

    • 存在栈, LaunchActivity 为 singleTop,且当前页面为 LaunchActivity,回调 onNewIntent

    • 存在栈, LaunchActivity 为其他需要切换栈的 launchMode,在任务栈顶启动 LaunchActivity

    • Android 12,存在栈,且栈底为 singleInstancePerTask,调起任务栈

  • Android 12 中,任务栈切换 task,不会销毁原本任务记录,切换到桌面才会

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容