Activity 启动模式与任务栈
前言
Activity 启动模式最常见的误区,是把它背成四个词:standard、singleTop、singleTask、singleInstance。但真实项目里的问题通常不是“某个模式定义是什么”,而是下面这些现象:
- 点击通知后打开了两个首页,返回路径变得混乱。
- Deep Link 命中了已经存在的页面,但页面数据没有刷新。
- 登录页、首页、扫码页用了
singleTask,结果任务栈被清掉,用户返回不到预期页面。 - 从非 Activity 场景启动页面时漏了
FLAG_ACTIVITY_NEW_TASK。 - Android 12 以后在根启动页按返回,表现和旧系统不一致。
- 多窗口、最近任务、文档模式让“一个 App 一个任务栈”的假设失效。
本文的核心目标不是鼓励你到处配置启动模式,而是建立一个可调试的模型:Android 启动 Activity 时,系统要先决定“复用哪个实例、放进哪个 Task、是否清理上方页面、是否回调 onNewIntent()”。理解这个决策链,才能在通知、Deep Link、第三方 SDK、登录态重置和跨 App 跳转中做出稳定的导航设计。
本文基于 Android Developers 官方文档、Android 16 / API 36 文档以及 AOSP main 分支源码复查。不同厂商系统和具体 Android 分支可能有定制,涉及任务栈边界时建议用 adb shell dumpsys activity activities 做实机验证。
前置知识
建议先掌握:
- A020:Activity 生命周期完整解析。
Intent、Activity、AndroidManifest.xml的基本用法。onCreate()、onResume()、onDestroy()与onNewIntent()的区别。- Jetpack Navigation 或自研路由的基本返回栈概念。
A020 关注单个 Activity 实例如何创建、暂停、停止和销毁;本文关注多个 Activity 实例如何被组织到 Task 与 Back Stack 中。下一篇 A022 会讨论 Fragment 生命周期与状态恢复。
核心概念
Task 与 Back Stack
官方文档把 Task 定义为用户完成某件事情时交互的一组 Activity;这些 Activity 按打开顺序形成 Back Stack。默认情况下,新 Activity 会压入当前任务栈顶部,用户按返回时,栈顶 Activity 被结束,下面的 Activity 恢复。
可以把默认行为理解成:
A -> start B -> start C
Task 1:
bottom [A, B, C] top
Back:
C finish, B resume
Back:
B finish, A resume
重点有三个:
- Activity 实例可以重复出现。默认
standard模式下,多次启动同一个 Activity 会产生多个实例。 - Task 可以进入后台并保留栈。用户回到最近任务或启动器入口时,通常会恢复栈顶 Activity。
- 多窗口环境下,系统会按窗口管理任务或任务组,不能简单假设全局只有一个当前任务。
launchMode 与 Intent flags
控制启动行为有两类入口:
| 类型 | 配置位置 | 代表能力 | 特点 |
|---|---|---|---|
Manifest launchMode | <activity android:launchMode="..."> | 声明目标 Activity 的复用规则 | 稳定、全局生效,适合入口页或特殊容器 |
| Intent flags | intent.addFlags(...) | 调用方表达本次启动诉求 | 更灵活,适合通知、Deep Link、跨任务启动 |
官方文档强调:Manifest 和 Intent flags 都能影响启动模式;两者不是完全一一对应。实际源码里还会根据 documentLaunchMode、taskAffinity、调用方是否有 Activity、是否请求 result、是否多窗口等条件修正 flags。
五种 launchMode
截至 Android 16 / API 36 文档,android:launchMode 支持五种主要取值:
| launchMode | 实例策略 | Task 策略 | 典型场景 | 风险 |
|---|---|---|---|---|
standard | 每次创建新实例 | 默认进入调用方所在 Task | 普通页面 | 重复打开时栈变深 |
singleTop | 栈顶已是目标实例则复用 | 仍在当前 Task | 通知刷新当前页、搜索页 | 目标不在栈顶时仍会新建 |
singleTask | 查找可复用实例,存在则回调 onNewIntent() | 通常作为任务根或复用同 affinity 任务 | 启动器入口、浏览器式入口 | 会清理目标上方 Activity |
singleInstance | 全局独占实例 | 所在 Task 只容纳它自己 | 极少数强隔离入口 | 返回路径和多任务体验容易异常 |
singleInstancePerTask | 每个 Task 最多一个实例 | 可配合 FLAG_ACTIVITY_MULTIPLE_TASK 或文档模式生成多个任务 | 文档式、多实例入口 | 行为复杂,需要实测最近任务 |
大多数业务页面应保持 standard。singleTask 和 singleInstance 不是“防重复点击”的通用解法,它们改变的是任务栈语义,而不是按钮节流。
onNewIntent 不是 onCreate 的替代品
当系统复用已有 Activity 实例时,通常不会重新走 onCreate(),而是调用 onNewIntent()。AOSP Activity.java 的 API 注释也提醒:getIntent() 默认仍返回最初启动 Activity 的 Intent;如果后续逻辑依赖最新 Intent,应在 onNewIntent() 中调用 setIntent(intent)。
class ProductActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
renderFromIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
renderFromIntent(intent)
}
private fun renderFromIntent(intent: Intent) {
val productId = intent.getStringExtra(EXTRA_PRODUCT_ID) ?: return
// 将 productId 交给 ViewModel,而不是直接在 Activity 中堆业务逻辑。
}
companion object {
const val EXTRA_PRODUCT_ID = "product_id"
}
}
如果页面是 Compose + ViewModel 架构,推荐把 Intent 解析成稳定事件,再交给 ViewModel 更新 UI State;不要在 onNewIntent() 里直接重建整棵 UI。
整体架构
启动模式的决策跨越 App 进程和 system_server。调用方发起 startActivity(),Framework 通过 Binder 进入 ActivityTaskManagerService 体系,最终由 WindowManager/Task 相关类决定目标 Activity 放在哪里。
flowchart TD
Caller["调用方 Activity/Context"]
Intent["Intent + flags"]
ATMS["system_server: ActivityTaskManager"]
Starter["ActivityStarter"]
Record["ActivityRecord"]
Task["Task / Back Stack"]
Existing["复用已有实例"]
NewOne["创建新实例"]
Callback["app main thread: onNewIntent/onCreate"]
Caller --> Intent
Intent --> ATMS
ATMS --> Starter
Starter --> Record
Starter --> Task
Task --> Existing
Task --> NewOne
Existing --> Callback
NewOne --> Callback
这张图要关注两个边界:
- 任务栈决策主要发生在
system_server,不是 Activity 自己决定。 onCreate()和onNewIntent()最终回到目标 App 进程主线程执行,所以回调里仍要避免阻塞。
工作流程
默认 standard:直接压栈
默认 standard 最符合栈模型:无论目标 Activity 是否已经存在,只要没有特殊 flags,系统都会创建新实例并压入当前 Task。
初始:
Task: A -> B
B start C:
Task: A -> B -> C
C start B:
Task: A -> B -> C -> B(new)
这种模式适合绝大多数内容页、编辑页和流程页,因为用户的返回路径和打开路径一致。
singleTop:只复用栈顶
singleTop 的判断条件很窄:目标 Activity 必须已经位于当前栈顶。否则它和 standard 一样会新建实例。
场景 1:
Task: A -> Search
start Search
结果: A -> Search(onNewIntent)
场景 2:
Task: A -> Search -> Detail
start Search
结果: A -> Search -> Detail -> Search(new)
它适合“当前页接收新参数刷新”的场景,例如搜索页再次收到关键词、通知点击刷新当前会话页。它不适合处理“栈中任意位置只保留一个实例”的需求。
singleTask:复用任务中的根或已有实例,并清理上方页面
singleTask 的典型效果是:如果系统找到可复用实例,会把它所在任务带到前台,并把目标 Activity 上方的 Activity 清掉,然后通过 onNewIntent() 分发新 Intent。
初始:
Task: Home(singleTask) -> Detail -> Settings
start Home:
Task: Home(onNewIntent)
Detail/Settings finish
这个特性常用于启动器入口、某些 Deep Link 分发页或浏览器式入口。但它的副作用很强:目标上方页面会被结束,用户可能丢失未提交状态。不要因为“首页只要一个实例”就盲目把首页改成 singleTask;更常见的现代做法是在单 Activity + Navigation 栈内做 popUpTo 或状态路由。
singleInstance 与 singleInstancePerTask:极少数场景再用
singleInstance 会让目标 Activity 成为所在 Task 的唯一成员;它启动的其他 Activity 会进入别的 Task。这个模式会明显改变最近任务和返回体验,普通业务页几乎不应使用。
singleInstancePerTask 是更细粒度的单任务单实例模式。它允许在不同 Task 中存在多个目标实例,尤其可能和 FLAG_ACTIVITY_MULTIPLE_TASK、FLAG_ACTIVITY_NEW_DOCUMENT 等组合出现。它适合文档式、多实例入口,但调试成本高,必须结合最近任务、多窗口和 Deep Link 做完整测试。
Intent flags 的常见组合
| flags | 行为 | 常见用途 | 注意点 |
|---|---|---|---|
FLAG_ACTIVITY_SINGLE_TOP | 栈顶是目标时复用 | 通知刷新当前页 | 不处理非栈顶实例 |
FLAG_ACTIVITY_NEW_TASK | 在新 Task 中启动或把已有匹配 Task 带前台 | 非 Activity Context、通知、跨 App | 不能和 startActivityForResult 语义混用 |
FLAG_ACTIVITY_CLEAR_TOP | 如果目标已在当前 Task,清掉它上方页面 | 回到首页、登录态失效后回根页 | 目标可能被重建或走 onNewIntent(),取决于模式和 flags |
FLAG_ACTIVITY_CLEAR_TASK + NEW_TASK | 清空目标 Task 后以新 Activity 为根 | 退出登录后重建入口栈 | 破坏性强,只用于明确重置流程 |
FLAG_ACTIVITY_MULTIPLE_TASK | 跳过复用匹配 Task,强制新 Task | 启动器或文档式多实例 | 官方注释建议普通应用谨慎使用 |
FLAG_ACTIVITY_NEW_DOCUMENT | 创建或复用文档任务 | 文档、文件、浏览上下文 | 与 Manifest 文档模式、最近任务策略相关 |
一个实用原则:如果你只是想在 App 内部清理返回栈,优先使用 Navigation 或显式结束流程;如果你要跨通知、桌面图标、外部 Deep Link 影响系统任务,才考虑 Activity flags。
API 使用
1. Manifest 中声明启动模式
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
启动器入口一般不需要随意改成 singleTask。如果只是希望重复点击桌面图标时复用当前顶部首页,singleTop 往往更温和;如果你的入口需要处理外部 Deep Link,也要确保 onNewIntent() 能正确更新路由。
2. 通知点击打开已有会话页
fun chatPendingIntent(context: Context, conversationId: String): PendingIntent {
val intent = Intent(context, ChatActivity::class.java)
.putExtra(ChatActivity.EXTRA_CONVERSATION_ID, conversationId)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
return PendingIntent.getActivity(
context,
conversationId.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
如果 ChatActivity 正在栈顶,这次点击会走 onNewIntent();如果用户当前在其他页面,系统仍可能创建新的 ChatActivity。如果产品要求“无论在哪里都回到唯一会话页”,就不该只靠 singleTop,而要设计明确的根入口和内部导航策略。
3. 退出登录后重建任务
fun restartToLogin(context: Context) {
val intent = Intent(context, LoginActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
}
这个组合会清空关联 Task,再以登录页作为新根页面。它适合“登录态彻底失效,历史页面不能再返回”的安全场景;不适合普通页面跳转。
4. Deep Link 入口只做分发
class DeepLinkActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dispatch(intent)
finish()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
dispatch(intent)
finish()
}
private fun dispatch(intent: Intent) {
val target = parseTarget(intent.data)
startActivity(MainActivity.createIntent(this, target))
}
}
分发页应尽量无 UI、无重业务、可重复处理 Intent。真正的页面状态由主 Activity 或业务导航栈接管。
源码分析
本文复查的主要源码路径是 AOSP main 分支:
frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.javaframeworks/base/services/core/java/com/android/server/wm/ActivityRecord.javaframeworks/base/core/java/android/content/Intent.javaframeworks/base/core/java/android/app/Activity.javaframeworks/base/core/java/android/content/pm/ActivityInfo.java
启动决策主链路
在当前 AOSP ActivityStarter 中,启动请求大致经过:
executeRequest()
-> startActivityUnchecked()
-> startActivityInner()
-> computeTargetTask()
-> deliverToCurrentTopIfNeeded()
-> setTargetRootTaskIfNeeded()
-> resume or deliver new intent
这不是完整调用栈,而是与启动模式最相关的决策主线。
executeRequest() 负责前置校验、权限、调用方和参数整理。startActivityUnchecked() 和 startActivityInner() 进入任务栈决策。源码里会读取 ActivityRecord.launchMode,并结合 Intent flags 得到最终启动策略。
singleTop 的源码判断
ActivityStarter.deliverToCurrentTopIfNeeded() 会检查当前顶部 Activity 是否与目标组件一致、用户一致、进程已附着,并且满足 FLAG_ACTIVITY_SINGLE_TOP 或 LAUNCH_SINGLE_TOP。满足时,不创建新 Activity,而是把 Intent 交给已有实例。
这解释了为什么 singleTop 只对栈顶有效:源码判断的目标就是 top running Activity,而不是全栈搜索。
singleTask / CLEAR_TOP 的清栈行为
ActivityStarter 在处理 FLAG_ACTIVITY_CLEAR_TOP、文档复用、LAUNCH_SINGLE_TASK、LAUNCH_SINGLE_INSTANCE、LAUNCH_SINGLE_INSTANCE_PER_TASK 时,会进入“找到目标并清理其上方 Activity”的路径。官方文档也明确说明,singleTask 和 singleInstancePerTask 会移除目标 Activity 上方的 Activity,并把新 Intent 交给已有实例。
工程上最重要的结论是:singleTask 的副作用不是“少创建一个对象”,而是“改变返回栈结构”。它可能触发上方页面 onPause()、onStop()、onDestroy(),未保存草稿、未完成支付确认、临时选择状态都可能丢失。
NEW_TASK 与 taskAffinity
ActivityRecord 保存了 taskAffinity 和 launchMode 等来自 ActivityInfo 的信息。ActivityStarter 在 FLAG_ACTIVITY_NEW_TASK、singleTask、singleInstance 等情况下,会尝试把 Activity 放入合适的已有 Task,或创建新 Task。
这也是 taskAffinity 容易引发迷惑的原因:它不是“包名分组”这么简单,而是参与系统选择目标 Task。除非你明确在做跨应用任务归属、文档模式或特殊入口,不建议随意改默认 affinity。
Activity.onNewIntent 的细节
Activity.java 对 onNewIntent() 的注释强调两点:
- 复用已有实例时,新的 Intent 会分发到
onNewIntent()。 getIntent()仍可能返回最初的 Intent,如需后续使用新 Intent,应调用setIntent(intent)。
这解释了很多 Deep Link Bug:页面确实收到了新链接,但业务代码在 onResume() 里读取 getIntent(),拿到的却是旧参数。
Android 12+ 根启动页返回行为
官方任务栈文档说明:Android 12 及以上,用户在根 launcher Activity 按返回时,系统默认将 Activity 及其 Task 移到后台,而不是像 Android 11 及以下那样直接 finish。这个变化会影响“按返回退出 App”的历史实现。
如果你为了兼容旧逻辑在根 Activity 里覆写 onBackPressed() 并直接 finish(),可能会破坏 Android 12+ 预期体验。现代代码应使用 AndroidX Activity 的返回处理 API,并在没有拦截者时交给系统默认行为。
实战案例
案例一:通知点击导致重复首页
问题:通知 PendingIntent 打开 MainActivity,用户已经在 App 内某个二级页,点击通知后出现第二个首页,返回路径变成 Home -> Detail -> Home。
推荐排查:
- 检查 PendingIntent 是否每条通知 requestCode 相同导致参数覆盖。
- 检查
MainActivity是否真的需要系统级singleTask。 - 如果是单 Activity 架构,优先让
MainActivity接收 Intent 后交给 Navigation 处理目标页面。 - 对重复点击通知,确保
onNewIntent()能更新导航事件。
常见方案:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleRouteIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleRouteIntent(intent)
}
private fun handleRouteIntent(intent: Intent) {
val route = intent.getStringExtra(EXTRA_ROUTE) ?: return
// 将 route 发送给导航层;避免在这里直接 new Fragment 或重建 Activity。
}
}
案例二:退出登录清空历史页面
退出登录、账号被踢、Token 被服务端撤销时,历史页面不应该再通过返回键访问。此时使用 NEW_TASK | CLEAR_TASK 是合理的,因为需求本身就是重建任务根。
但如果只是“从详情页回首页”,不要用这个组合;它会让任务历史消失,用户也无法通过系统返回恢复原路径。
案例三:扫码页是否应该 singleTask
扫码页经常被误设成 singleTask,理由是“相机页面不能开多个”。更稳妥的做法通常是:
- UI 层禁用重复点击或用互斥状态防止重复启动。
- 相机资源绑定
onResume()/onPause()或生命周期感知组件。 - 扫码结果通过明确的返回协议传递。
singleTask 不能替代资源互斥管理。它只是在系统任务栈层面改变 Activity 复用与清栈行为。
性能优化
启动模式本身不是性能优化工具。错误使用启动模式反而会制造额外成本:
singleTask/CLEAR_TOP可能批量 finish 上方页面,触发多个生命周期回调和状态保存。onNewIntent()中做重解析、同步 IO 或复杂路由,会阻塞主线程首帧。CLEAR_TASK会重建任务根,导致缓存页面全部失效。- 多任务和文档模式会增加最近任务条目,影响用户恢复路径。
建议:
- 用启动模式解决任务归属问题,用防抖解决重复点击,用导航栈 API 解决 App 内返回路径。
onNewIntent()只做轻量解析和事件分发,耗时工作交给 ViewModel 与协程。- 对 Deep Link、通知和桌面入口写自动化或手工回归用例,覆盖冷启动、热启动、后台恢复、进程死亡后恢复。
- 用
adb shell dumpsys activity activities、adb shell am start -W ...、Logcat 生命周期日志验证真实栈结构。
示例调试命令:
adb shell am start -W \
-n com.example.app/.MainActivity \
-a android.intent.action.VIEW \
-d "example://product/42"
adb shell dumpsys activity activities | sed -n '/ACTIVITY MANAGER ACTIVITIES/,/Run #0/p'
常见问题
1. singleTop 为什么有时不回调 onNewIntent()?
因为目标 Activity 必须已经位于栈顶。只要目标不在顶部,singleTop 仍会创建新实例。还要确认启动的是同一个组件、同一个用户空间,并且没有被其他 flags 或任务边界改变。
2. singleTask 是否保证全 App 只有一个实例?
不要这样表述。官方语义与源码实现都和 Task、affinity、document/multiple task 条件有关。一般情况下它会复用已有任务中的实例并清理上方页面,但多窗口、文档模式、singleInstancePerTask 等场景会让“唯一”的直觉失效。
3. onNewIntent() 里必须调用 setIntent() 吗?
如果后续代码会通过 getIntent() 读取最新参数,就应该调用。否则 getIntent() 可能仍是初始 Intent。更好的方式是把 Intent 解析成业务事件,交给状态层处理。
4. 通知打开页面应该用哪个模式?
没有固定答案。普通内容页可以保持 standard;如果当前栈顶就是目标页并希望刷新,可用 FLAG_ACTIVITY_SINGLE_TOP 或 singleTop;如果通知必须回到统一主入口,再由内部导航分发,通常让 MainActivity 处理 Intent 更可控。
5. App 根页面按返回为什么 Android 12 以后不像以前那样 finish?
Android 12 及以上,根 launcher Activity 按返回默认更接近 Home 行为:把任务移到后台。这样用户再次进入时更可能从 warm state 恢复。不要为了复刻旧行为随意 finish() 根页面。
6. Jetpack Navigation 还需要 launchMode 吗?
需要,但少很多。Navigation 解决的是 App 内目的地栈;Activity launchMode 解决的是系统 Task 与 Activity 实例复用。单 Activity 架构下,更多问题应在 Navigation 层处理,Activity 启动模式只负责入口语义。
面试考点
基础题
- Task 和 Back Stack 的区别是什么?
standard和singleTop的差异是什么?- 为什么
singleTop不在栈顶时仍会创建新实例? onNewIntent()和onCreate()分别在什么情况下调用?FLAG_ACTIVITY_NEW_TASK常见使用场景是什么?
进阶题
singleTask为什么会清理目标 Activity 上方页面?FLAG_ACTIVITY_CLEAR_TOP与singleTask有什么相似和差异?- Manifest
launchMode和 Intent flags 同时存在时应该如何分析? taskAffinity如何影响目标 Task 选择?- Android 12+ 根 launcher Activity 的返回行为有什么变化?
源码题
ActivityStarter在启动 Activity 时大致经历哪些方法?deliverToCurrentTopIfNeeded()为什么能解释singleTop行为?ActivityRecord保存了哪些与启动模式相关的信息?Activity.onNewIntent()为什么建议配合setIntent()?- 如何用
dumpsys activity验证任务栈?
设计题
- 设计通知点击进入聊天页,如何避免重复页面和参数覆盖?
- 登录过期后如何清空历史页面,同时保证用户体验可解释?
- Deep Link 冷启动、热启动、后台恢复分别如何处理?
- 单 Activity 架构下,哪些问题应该交给 Navigation,而不是 launchMode?
- 第三方 SDK 要求入口页
singleTask,你如何评估副作用?
总结
Activity 启动模式可以用一个决策模型概括:
- 默认
standard:新实例压栈,返回路径最直观。 singleTop:只复用栈顶,适合当前页接收新 Intent。singleTask:复用已有任务实例并清理上方页面,适合少量入口,不适合普通防重复。singleInstance/singleInstancePerTask:改变任务隔离语义,只在明确多任务或强隔离场景使用。- Intent flags 表达本次启动诉求,但最终行为还会受 launchMode、taskAffinity、document 模式、多窗口和系统版本影响。
工程实践中,先设计用户返回路径,再选择启动模式。App 内导航优先交给 Navigation 或自研路由;系统任务入口才使用 Activity launchMode 和 Intent flags。遇到疑难问题,不要靠猜,直接用生命周期日志和 dumpsys activity 看真实 Task。
扩展阅读
- Android Developers: Tasks and the back stack
developer.android.com/guide/compo… - Android Developers:
<activity>manifest element
developer.android.com/guide/topic… - Android Developers API reference:
Intentactivity flags
developer.android.com/reference/a… - Android Developers API reference:
Activity.onNewIntent()
developer.android.com/reference/a… - AOSP:
frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
android.googlesource.com/platform/fr… - AOSP:
frameworks/base/services/core/java/com/android/server/wm/ActivityRecord.java
android.googlesource.com/platform/fr… - AOSP:
frameworks/base/core/java/android/content/Intent.java
android.googlesource.com/platform/fr… - AOSP:
frameworks/base/core/java/android/app/Activity.java
android.googlesource.com/platform/fr… - Skill references:
references/workflow/article_pipeline.md,references/workflow/research.md,references/rules/source_claims.md,references/rules/quality.md