在 Android四大组件之 Activity 里我们说过:系统用一个来管理 Activity,“打开新页面就往栈顶放盘子,按返回就拿走栈顶盘子”。这是最简单的模型。但真实应用里有很多需求打破了这个简单模型:点通知不想重复打开同一个页面、从分享进来希望复用已有页面、主页希望始终只有一个……这些都靠启动模式(launchModel) 和**任务栈(Task)**来控制。这是 Activity 知识里最绕的一块,本文把它讲透。

一、先搞懂两个概念:Task 与 Back Stack

1.1 Task(任务)

Task 是用户完成某件事时所接触的一组 Activity 的集合。 它不等于应用——一个 Task 里的 Activity 可以来自不同应用。

举例:你在微信里点了一个链接,跳到浏览器看网页,再从网页分享回某个应用。这一连串 Activity(微信页 → 浏览器页 → 分享页)可能都待在同一个 Task 里,按返回键会依次退回。

1.2 Back Stack(返回栈)

每个 Task 内部用一个来组织 Activity,这就是返回栈。它遵循后进先出(LIFO)

Activity任务栈

  • 启动新 Activity → 压入栈顶(push),它显示出来;
  • 按返回键 → 弹出栈顶(pop)并销毁,下面那个重新显示;
  • 栈空了 → Task 结束,回到桌面。

关键认知: 默认情况下,每次 startActivity() 都会创建一个新实例压栈——哪怕是同一个 Activity,连点三次就有三个实例叠在一起。启动模式要解决的,正是"何时复用、何时新建"。


二、四种启动模式(launchMode)

启动模式在 AndroidManifest.xml 中通过 android:launchMode 声明:

<activity android:name=".MainActivity" android:launchMode="singleTask" />

2.1 standard(标准模式,默认)

每次启动都创建一个新实例,压入当前 Task 栈顶。 可以有任意多个实例,可以分布在不同 Task。

启动 A、A、A:  [A1, A2, A3]   ← 三个独立实例
  • 场景: 绝大多数普通页面。
  • 特点: 最简单,符合"一个动作一个页面"的直觉。

2.2 singleTop(栈顶复用)

如果目标 Activity 已经在栈顶,就复用它,不创建新实例;否则和 standard 一样新建。复用时会回调 onNewIntent()

当前 [A, B],再启动 B:  [A, B]      ← B 在栈顶,复用,触发 onNewIntent
当前 [A, B],再启动 A:  [A, B, A]   ← A 不在栈顶,新建
  • 场景: 通知点击最典型。连续收到多条通知点进同一个页面,不希望叠出一堆重复页面;搜索结果页反复搜索也常用。
  • 要点: 复用时数据通过 onNewIntent(intent) 传入,记得在这里更新界面,而不是只看 onCreate 里的旧数据。

2.3 singleTask(栈内复用,单实例于 Task)

整个 Task 里只允许存在一个该 Activity 的实例。 启动时:

  • 若实例已存在,把它之上的所有 Activity 全部清除(弹出销毁),让它回到栈顶,并回调 onNewIntent()
  • 若不存在,则新建。
当前 [A, B, C, D],启动 B(singleTask):
   → 清除 C、D,变成 [A, B],B 触发 onNewIntent
  • 场景: 应用的主界面 / 首页。无论从哪里回到主页,都希望它是唯一的,并清掉它上面堆积的页面。
  • 要点: 它有"清栈到自己"的副作用(clear top),上面的页面会被销毁。

2.4 singleInstance(全局单实例,独占一个 Task)

该 Activity 独占一个全新的 Task,且这个 Task 里永远只有它一个。 任何从它启动的其他 Activity 都会被放到别的 Task。

  • 场景: 来电界面、系统级悬浮入口等需要"全局唯一、独立存在"的页面。日常业务极少用到
  • 要点: 它的 Task 里只有它自己,行为特殊,不要轻易使用。

四种模式对比

模式 是否复用 复用条件 是否触发 onNewIntent 典型场景
standard —— 普通页面(默认)
singleTop 已在栈顶 通知、搜索结果页
singleTask Task 内已存在(会清栈) 主界面/首页
singleInstance 独占 Task 来电、全局唯一页

三、用 Intent Flag 动态控制启动行为

launchMode 写死在 Manifest 里。但很多时候我们想在代码里临时改变启动行为,这就用 Intent Flag。当两者冲突时,Flag 的优先级更高

val intent = Intent(this, MainActivity::class.java).apply {
    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
startActivity(intent)

常用 Flag

Flag 作用 对应/类似
FLAG_ACTIVITY_NEW_TASK 在新 Task 中启动(从非 Activity 上下文如 Service 启动 Activity 时必须加) ——
FLAG_ACTIVITY_SINGLE_TOP 栈顶复用 singleTop
FLAG_ACTIVITY_CLEAR_TOP 若目标已在栈中,清除它之上的所有 Activity 配合实现 singleTask 效果
FLAG_ACTIVITY_CLEAR_TASK 启动前清空整个 Task(常与 NEW_TASK 连用) 退出登录回到登录页常用
FLAG_ACTIVITY_NO_HISTORY 该 Activity 离开后不留在栈中 一次性页面

退出登录的经典写法: NEW_TASK | CLEAR_TASK 启动登录页,清空所有历史,用户按返回也回不到登录前的页面。


四、影响任务栈的其他属性

除了 launchMode,Manifest 里还有几个属性会影响 Task 行为,了解即可:

属性 作用
taskAffinity 指定 Activity "亲和"哪个 Task(默认是应用包名)。配合 singleTask / NEW_TASK 时决定它进入哪个 Task
allowTaskReparenting 允许 Activity 在条件满足时"改换门庭"到它更亲和的 Task
clearTaskOnLaunch 每次从桌面重新启动应用时,清空根 Activity 之上的所有页面
alwaysRetainTaskState 让系统不要自动清理该 Task 的状态

建议: taskAffinity 等属性容易把任务栈搞乱、引发难以排查的导航 Bug,没有明确需求不要碰。绝大多数应用只用 launchMode + 少数几个 Flag 就够了。


五、onNewIntent:复用实例时别忘了它

只要发生了"复用已有实例"(singleTop / singleTask 命中、或 SINGLE_TOP / CLEAR_TOP 命中),系统不会再走 onCreate(),而是回调:

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    // 复用时新数据从这里来,必须在此更新界面
    setIntent(intent)              // 更新 Activity 持有的 intent
    val id = intent.getIntExtra("id", -1)
    refreshUi(id)
}

最常见的 Bug: 用 singleTop/singleTask 的页面,复用时界面没更新——因为开发者只在 onCreate 里读了 Intent。记住:复用走的是 onNewIntent,要在这里重新取数据并刷新。


六、现代视角:单 Activity 架构下还需要启动模式吗?

如前面 Fragment 篇 所讲,现代应用流行单 Activity 架构——一个 Activity 壳 + 多个 Fragment/Compose 目的地,页面跳转交给 Navigation 组件管理。

在这种架构下:

  • 应用内的"页面跳转"大多发生在 Fragment / Compose 之间,用不到 launchMode
  • 但只要还涉及多 Activity、通知跳转、跨应用回流、第三方 SDK 入口,启动模式与任务栈依然是绕不开的基础;
  • Navigation 也提供了对应概念(如 launchSingleToppopUpTo)来表达类似"栈顶复用""清栈"的语义。

一句话: 即便你写的是单 Activity 应用,理解 Task 和启动模式仍然必要——因为系统级的导航、通知、跨应用跳转始终运行在这套机制之上。


参考来源:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐