启动模式概览
| 启动模式 | 声明方式 | 行为特点 | 典型场景 |
|---|---|---|---|
| standard(标准模式) | 默认或android:launchMode="standard" | 每次都创建新实例 | 普通页面跳转,如列表→详情 |
| singleTop(栈顶复用) | android:launchMode="singleTop" | 如果已经在栈顶,则复用并调用onNewIntent() | 避免重复创建,如搜索 |
| singleTask(栈内唯一) | android:launchMode="singleTask" | 在任务栈中保持唯一,会清除上方的Activity | 应用主页、登录页 |
| singleInstance(单实例) | android:launchMode="singleInstance" | 独占一个任务栈,且栈内只有它自己,确保全局只有一个实例 | 系统Launcher、来电页面、视频播放 |
| singleInstancePerTask(单实例每任务) | android:launchMode="singleInstancePerTask" | 允许同一个 Activity 在不同任务栈中有自己的实例,但在同一栈中保持唯一 | 多窗口或多实例场景,如文档编辑 |
任务栈是是一个后进先出的Activity集合,用于维护用户的导航历史。每一个Activity都有一个taskAffinity属性,默认是应用包名,它决定了Activity倾向于在哪个任务栈中。
启动模式详解
standard(标准模式)
标准模式是Activity的默认启动模式。在这种模式下,每次启动Activity都会创建一个新的实例,并将其压入当前任务栈的栈顶。
当在应用中调用startActivity()启动一个standard模式的Activity时,系统会执行一下内容,与Activity启动流程最后一步一致。
// 在ActivityThread中处理standard模式的启动
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// 1. 通过类加载器创建Activity实例
Activity activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
// 2. 调用Activity的构造函数
// 3. 创建Context实例
// 4. 调用attach()方法绑定上下文
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title,
r.parent, r.embeddedID, r.lastNonConfigurationInstances,
config, r.referrer, r.voiceInteractor, window);
// 5. 调用onCreate()生命周期方法
mInstrumentation.callActivityOnCreate(activity, r.state);
return activity;
}
假如我们有三个Activity,A(standard) → B(standard) → C(standard)
初始状态:
任务栈:[A]
操作1:从A启动B
结果:创建B的新实例
任务栈:[A, B]
操作2:从B启动C
结果:创建C的新实例
任务栈:[A, B, C]
操作3:从C再次启动B
结果:创建B的另一个新实例
任务栈:[A, B, C, B] ← 注意:这是B的第二个实例
操作4:按返回键
结果:销毁栈顶的B实例
任务栈:[A, B, C]
继续按返回键:C → B → A
最终回到空栈
- 设计意图:提供最直观的页面导航,保持每个实例的独立性,支持多实例并发存在。
- 适用场景:大多数普通的内容展示页面
- 不适用场景:应用主页、登录页、全局设置页(容易创建多实例,应保持唯一性)
singleTop(栈顶复用)
singleTop模式的核心特性是避免在栈顶重复创建相同Activity的实例。如果目标Activity已经在任务栈的栈顶,则复用该实例并调用其onNewIntent()方法;否则,创建新实例。
AMS在启动singleTop Activity时的决策逻辑:
// ActivityStarter.java - 判断是否复用栈顶实例
private boolean startActivityUnchecked(...) {
// 检查是否是singleTop模式
if (r.launchMode == ActivityInfo.LAUNCH_SINGLE_TOP) {
// 获取当前任务栈的栈顶Activity
ActivityRecord top = mSourceRecord.task.getTopActivity();
if (top != null && top.realActivity.equals(r.realActivity)) {
// 栈顶已有相同Activity,触发deliverNewIntent
mTargetStack.lastPausedActivity = top;
// 调用onNewIntent()而不是创建新实例
top.deliverNewIntentLocked(callingUid, r.intent);
// 调整任务栈顺序
mTargetStack.moveTaskToFrontLocked(top.task, noAnimation, options,
r.appTimeTracker, "singleTop");
return true;
}
}
// 否则继续正常启动流程
return startActivityLocked(r, newTask, doResume, keepCurTransition, options);
}
栈顶已存在相同Activity
初始状态:
任务栈:[A, B(singleTop)]
操作:从B启动B
结果:复用B的现有实例,调用B.onNewIntent()
任务栈:[A, B] ← 栈深度不变
日志:B.onNewIntent()被调用,B.onResume()被调用
栈顶不是目标Activity
初始状态:
任务栈:[A(singleTop), B]
操作:从B启动A
结果:创建A的新实例
任务栈:[A, B, A] ← 新实例入栈
日志:A.onCreate()被调用
复杂导航链
初始状态:
任务栈:[A, B(singleTop), C]
操作1:从C启动B
结果:创建B的新实例(因为B不在栈顶)
任务栈:[A, B, C, B] ← 第二个B实例
操作2:从栈顶B再次启动B
结果:复用栈顶B实例
任务栈:[A, B, C, B] ← 栈深度不变
当singleTop Activity被复用时,系统不会调用onCreate(),而是调用onNewIntent(),需要手动调用setIntent()更新Activity的当前Intent,onNewIntent()调用后,onResume()会被调用。
- 设计意图:优化用户体验,避免重复页面闪烁;节省系统资源,减少不必要的实例创建;支持实时更新已存在的界面,例如后台运行的Activity需要根据特定事件更新界面。
- 适用场景:搜索页面,通知处理页面,实时数据展示页面。
- 局限性:只检查栈顶,不检查栈中其他位置,如果目标不在栈顶,仍会创建新实例
singleTask(栈内唯一)
singleTask模式确保Activity在整个任务栈中保持唯一性。当启动一个singleTask Activity时,系统会:
- 查找匹配的任务栈(基于taskAffinity)
- 如果找到栈中已有该Activity的实例,则清除其上的所有其他Activity
- 将找到的实例移到栈顶并调用
onNewIntent() - 如果没找到,则在合适的任务栈中创建新实例
taskAffinity是决定Activity属于哪个任务栈的关键属性:
<!-- 默认情况:使用应用包名 -->
<activity android:name=".MainActivity"
android:taskAffinity="com.example.myapp" />
<!-- 显式指定不同的affinity -->
<activity android:name=".SettingsActivity"
android:launchMode="singleTask"
android:taskAffinity="com.example.myapp.settings" />
<!-- 完全不同的affinity -->
<activity android:name=".ShareActivity"
android:launchMode="singleTask"
android:taskAffinity="com.example.share" />
taskAffinity规则:
- 默认值:应用包名
- 相同affinity的Activity倾向于在同一个任务栈
- 不同affinity的singleTask Activity会进入不同的任务栈
- 从桌面启动的应用默认创建新的任务栈
AMS处理singleTask启动的完整流程:
// ActivityStarter.java - singleTask的复杂处理逻辑
private int setTaskFromIntentRecord(ActivityRecord r) {
// 1. 检查是否应该创建新任务栈
boolean newTask = false;
boolean bringToFront = false;
// 2. 根据launchMode和Flags决定
if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0 &&
(r.intent.getFlags() & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) == 0) {
newTask = true;
}
// 3. 如果是singleTask,查找匹配的任务栈
TaskRecord task = null;
if (r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) {
task = findTaskLocked(r);
}
// 4. 如果找到任务栈
if (task != null) {
// 检查栈中是否已有该Activity
ActivityRecord existing = task.getActivity(r.intent, r.info);
if (existing != null) {
// 清除existing之上的所有Activity
while (task.getTopActivity() != existing) {
ActivityRecord top = task.getTopActivity();
if (top != null) {
top.finishIfPossible("singleTask");
}
}
// 移到栈顶并传递新Intent
task.moveActivityToFront(existing);
existing.deliverNewIntentLocked(callingUid, r.intent);
bringToFront = true;
}
}
// 5. 没找到则创建新任务栈
if (task == null && newTask) {
task = new TaskRecord(mSupervisor.getNextTaskId(),
r.info, r.intent, null, null);
mService.addTask(task, true);
}
return task != null ? task.taskId : -1;
}
清除上方Activity
初始状态:
任务栈:[A, B, C(singleTask), D, E]
操作:从E启动C(singleTask)
结果:找到C的实例,清除D和E,C回到栈顶
任务栈:[A, B, C]
日志:C.onNewIntent()被调用,D和E被销毁
不同affinity的singleTask
应用结构:
- MainActivity (standard, affinity=默认)
- LoginActivity (singleTask, affinity=默认)
- SettingsActivity (singleTask, affinity="com.example.settings")
操作流程:
1. 从桌面启动应用
任务栈1:[MainActivity]
2. Main启动Login
任务栈1:[MainActivity, LoginActivity]
3. Login启动Settings (不同affinity)
创建新任务栈2:[SettingsActivity]
现在有两个任务栈,Settings所在栈在前台
4. 从Settings返回
销毁Settings,回到任务栈1的Login
从其他应用启动singleTask
假设有应用A和应用B,B有Activity X(singleTask)
操作流程:
1. 在应用A中启动X
2. 系统会创建新任务栈(因为不同应用)
3. 任务栈A:[A的当前Activity]
任务栈B:[X]
4. 在X中启动A的某个Activity
5. 新Activity会进入任务栈A
- 设计意图:为应用提供明确的"主页"概念;防止关键页面出现多个实例;管理复杂的导航层次结构
- 适用场景:应用主界面,登录/认证页,全局设置页,深度链接处理页 (处理后返回主流程)。
singleInstance(单实例)
singleInstance是最严格的启动模式,它要求Activity独占一个任务栈,并且该栈中只有它一个Activity。系统范围内只能存在该Activity的一个实例。
// ActivityStarter.java - singleInstance的特殊处理
private int setTaskFromIntentRecord(ActivityRecord r) {
if (r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
// 查找是否已有singleInstance任务栈
TaskRecord existingTask = findTaskLocked(r);
if (existingTask != null) {
// 已有实例,直接切换到前台
mTargetStack = existingTask.stack;
ActivityRecord existing = existingTask.getTopActivity();
if (existing != null) {
existing.deliverNewIntentLocked(callingUid, r.intent);
}
// 确保任务栈在前台
mTargetStack.moveToFront("singleInstance");
return existingTask.taskId;
}
// 创建新的任务栈,并设置singleInstance标志
TaskRecord task = new TaskRecord(mSupervisor.getNextTaskId(),
r.info, r.intent, null, null);
task.setTaskToReturnTo(HOME_ACTIVITY_TYPE);
mService.addTask(task, true);
// 设置任务栈为singleInstance模式
task.mSingleInstanceMode = true;
return task.taskId;
}
return -1;
}
从其他Activity启动singleInstance
应用结构:
- MainActivity (standard)
- CallActivity (singleInstance)
- ChatActivity (standard)
操作流程:
1. 启动Main
任务栈1:[Main]
2. Main启动Call
创建新任务栈2:[Call]
栈2在前台,栈1在后台
3. Call启动Chat
创建新任务栈3:[Chat] ← 不会进入栈2!
或者进入栈1:[Main, Chat](如果允许)
4. 从Chat返回
回到Call所在的栈2
singleInstance Activity启动其他singleInstance
应用结构:
- VideoCallActivity (singleInstance)
- AudioCallActivity (singleInstance)
操作流程:
1. 启动VideoCall
任务栈1:[VideoCall]
2. VideoCall启动AudioCall
创建新任务栈2:[AudioCall]
每个singleInstance都有自己独立的任务栈
3. 此时有两个任务栈
栈1:[VideoCall]
栈2:[AudioCall]
- 设计意图:实现全局唯一的重要服务界面,防止与其他界面交互干扰,确保关键界面完全独立。
- 适用场景:来电/视频通话界面,系统Launcher,全局浮窗/画中画控制器。
- 注意项:谨慎使用singleInstance,它会破坏正常的导航栈,确保用户有明确的退出方式;考虑与其他应用的交互;在Android 10+上注意后台启动限制。
singleInstancePerTask(单实例每任务模式- Android 12+)
Android 12引入了singleInstancePerTask来解决传统启动模式在大屏幕设备(平板、折叠屏)上的局限性。随着多窗口、分屏模式的普及,传统模式无法很好地处理同一个Activity在多个任务栈中的实例问题。
singleInstancePerTask结合了singleTask和singleInstance的特点:
- 在同一个任务栈中保持唯一(类似singleTask)
- 允许在不同任务栈中存在多个实例(类似standard)
- 不强制独占任务栈,允许其他Activity进入
// Android 12+ 中的新处理逻辑
private int setTaskFromIntentRecord(ActivityRecord r) {
if (r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK) {
// 查找当前任务栈中是否已有该Activity
ActivityRecord existing = null;
if (mSourceRecord != null) {
existing = mSourceRecord.task.getActivity(r.intent, r.info);
}
if (existing != null) {
// 清除当前任务栈中该实例之上的所有Activity
mSourceRecord.task.finishActivitiesAboveLocked(existing);
// 传递新Intent
existing.deliverNewIntentLocked(callingUid, r.intent);
// 移到栈顶
mSourceRecord.task.moveActivityToFront(existing);
return existing.task.taskId;
}
// 当前栈中没有,创建新实例
// 注意:不寻找其他任务栈
return -1; // 表示创建新实例
}
return -1;
}
多窗口模式下的文档编辑
设备:平板,分屏模式
应用:文档编辑器
窗口1(左侧):
任务栈1:[DocList, DocEditor(singleInstancePerTask)]
窗口2(右侧):
任务栈2:[DocList, DocEditor(singleInstancePerTask)]
每个窗口有独立的DocEditor实例
但每个任务栈内DocEditor是唯一的
折叠屏的不同状态
// 在折叠屏设备上处理不同状态
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 监听折叠状态变化
val foldFeature = findViewById<FoldFeatureView>(R.id.fold_feature)
foldFeature?.setOnFoldStateChangedListener { isFolded ->
if (isFolded) {
// 折叠状态:单窗口
handleFoldedState()
} else {
// 展开状态:可多窗口
handleExpandedState()
}
}
}
private fun openDetailInNewTask(itemId: String) {
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("item_id", itemId)
// 在新任务栈中启动
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
}
startActivity(intent)
}
}
Intent Flags与启动模式的交互
Intent Flags是Android系统中用于精确控制Activity启动行为的标记集合,它们作为Intent对象的附加指令,影响Activity在任务栈中的管理方式,如是否创建新实例、如何处理现有栈状态或调整导航路径。
| Flag | 效果 | 与启动模式的冲突处理 |
|---|---|---|
FLAG_ACTIVITY_NEW_TASK | 在新任务栈中启动 | 会覆盖standard模式 |
FLAG_ACTIVITY_SINGLE_TOP | 等同于singleTop | 与Manifest的singleTop效果相同 |
FLAG_ACTIVITY_CLEAR_TOP | 清除目标Activity之上的所有Activity | 常与singleTask配合 |
FLAG_ACTIVITY_CLEAR_TASK | 清除整个任务栈 | 必须与NEW_TASK一起使用 |
使用示例:
Intent intent = new Intent(context, MyActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
重要原则:Intent Flags的优先级高于Manifest中声明的launchMode。
// 在ActivityStarter中处理Flags和launchMode的优先级
private int computeLaunchingTaskFlags() {
int launchFlags = mStartActivity.intent.getFlags();
// Flags可以覆盖launchMode
if ((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
if ((launchFlags & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
// 总是创建新任务栈
return FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK;
} else {
// 可能复用现有任务栈
return FLAG_ACTIVITY_NEW_TASK;
}
}
// 否则使用Manifest中的launchMode
switch (mStartActivity.launchMode) {
case ActivityInfo.LAUNCH_SINGLE_INSTANCE:
return FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK;
case ActivityInfo.LAUNCH_SINGLE_TASK:
return FLAG_ACTIVITY_NEW_TASK;
// ... 其他模式
}
}
启动模式选择指南
| 场景需求 | 推荐模式 | 替代方案 | 说明 |
|---|---|---|---|
| 普通页面跳转 | standard | - | 默认行为 |
| 避免重复打开同一页面 | singleTop | FLAG_ACTIVITY_SINGLE_TOP | 适用于搜索、通知页 |
| 应用主页/登录页 | singleTask | FLAG_ACTIVITY_CLEAR_TOP | 保证唯一性 |
| 全局唯一界面 | singleInstance | - | 来电、Launcher |
| 多窗口支持 | singleInstancePerTask | singleTask + 多taskAffinity | Android 12+ |
| 深度链接处理 | singleTask | - | 清除历史栈 |
Q&A
Q1:singleTask和FLAG_ACTIVITY_CLEAR_TOP有什么区别?
A:
singleTask:系统会寻找匹配的任务栈,如果找到该Activity实例,则清除其上的所有Activity并调用onNewIntent();如果没找到,则创建新实例。FLAG_ACTIVITY_CLEAR_TOP:清除目标Activity所在任务栈中它上面的所有Activity。如果目标Activity是standard模式,会销毁并重新创建;如果是singleTop/singleTask,会调用onNewIntent()。
Q2:如何实现应用内多任务?
A:
可以使用singleInstancePerTask(Android 12+)或结合singleTask+ 不同taskAffinity来实现。例如文档编辑器应用,每个文档在独立的任务栈中编辑。
Q3:从通知栏启动,如何回到正确的页面?
A:
需要根据业务逻辑设计合适的启动模式。如果是回到已有实例:
val intent = Intent(context, TargetActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
// 或者使用singleTask模式
}