2.3 实验:Activity 的四种启动模式
1.1 本次实验的项目参数(供参考)
本次已提供初始工程,无需手动新建项目。以下参数是该项目的基本信息,导入后可在 Android Studio 中核对:
| 参数名 | 值 |
|---|---|
APP_NAME | LaunchModeDemo |
PACKAGE_NAME | cn.edu.sziit.android.launchmodedemo |
MIN_SDK | API 24 |
1.2 下载实验记录表
本实验要求你边操作边填写实验记录表,完成后提交作为评分依据。
实验记录表:点击下载作业提交文档
1.3 导入项目
本次实验已为你准备好初始项目,无需从零新建。请按以下步骤操作:
步骤 1:点击下方链接下载项目压缩包:
📦 下载 LaunchModeDemo.zip步骤 2:将压缩包解压到合适目录。
⚠️ 解压路径要求:
- 不能含有中文字符
- 不能含有空格
- 建议放在非 C 盘的专用目录,例如:
D:\AndroidProjects\LaunchModeDemo
步骤 3:将解压后的项目导入 Android Studio,并完成 Gradle Sync。
参考《导入已有 Android 项目》完成导入与同步。
本实验不在此步创建 Activity,后续步骤将手动创建。
2.1 什么是任务栈(Back Stack)
Android 使用任务栈来管理页面的入栈和出栈。每次打开一个新 Activity,它就被压入栈顶;按 Back 键时,栈顶 Activity 出栈被销毁,下方的 Activity 重新可见。
按 Back 前: 按 Back 后:┌──────────────┐ ┌──────────────┐│ ActivityC │ ← 栈顶 │ ActivityB │ ← 栈顶├──────────────┤ ├──────────────┤│ ActivityB │ │ ActivityA │├──────────────┤ └──────────────┘│ ActivityA │ ← 栈底└──────────────┘2.2 四种启动模式概览
在 AndroidManifest.xml 中,通过 android:launchMode 属性为每个 Activity 指定启动模式:
| 启动模式 | 每次都创建新实例? | 栈顶复用? | 清栈? | 独立任务栈? |
|---|---|---|---|---|
standard(默认) | ✅ 是 | ❌ | ❌ | ❌ |
singleTop | 不在栈顶时创建 | ✅ 在栈顶时复用 | ❌ | ❌ |
singleTask | 栈中存在时复用 | — | ✅ 清除其上所有页面 | ❌(可配置) |
singleInstance | 只创建一次 | — | — | ✅ 独占新任务栈 |
2.3 用 adb 查看任务栈
后续每个实验步骤均使用以下命令查看任务栈。 本节说明命令各段的作用及输出格式。
使用
adb命令前,需完成环境变量配置,详见 1.1 搭建 Android 开发环境 · 步骤 7。
命令:
adb shell dumpsys activity activities | findstr /R /C:"^ \* Task.*launchmodedemo" /C:"Hist.*launchmodedemo"命令通过 | 管道连接两段:
| 片段 | 作用 |
|---|---|
adb shell dumpsys activity activities | 输出设备上所有 Activity 和任务栈的当前状态 |
findstr /R /C:"^ \* Task.*launchmodedemo" /C:"Hist.*launchmodedemo" | 过滤保留两类行:① 以 2 个空格开头的 Task 行(任务栈摘要);② 含 Hist 的行(栈内 Activity 记录) |
Task 行空格前缀的作用:
dumpsys输出中,同一个 Task 会出现在多个区块,但主任务列表中的 Task 行固定以 2 个空格开头,其他区块的引用缩进更深。^ \* Task前缀精确匹配主任务列表,避免重复输出。
输出格式(以 standard 模式、栈内 3 个 Activity 为例):
* Task{... A=...:cn.edu.sziit.android.launchmodedemo ... sz=3} * Hist #2: ActivityRecord{...} cn.edu.sziit.android.launchmodedemo/.MainActivity * Hist #1: ActivityRecord{...} cn.edu.sziit.android.launchmodedemo/.SecondActivity * Hist #0: ActivityRecord{...} cn.edu.sziit.android.launchmodedemo/.MainActivitysz=3:当前任务栈内共有 3 个 Activity 实例Hist #0为栈底(最早入栈),序号最大的为栈顶(最近打开)
本步骤创建实验所需的两个界面 MainActivity 和 SecondActivity。不配置任何启动模式,运行后观察 standard(默认)模式的行为:每次跳转都会新建一个界面,不管该界面之前是否已经打开过。
3.1 创建 MainActivity
在 app/src/main/java/cn/edu/sziit/android/launchmodedemo/ 下右键 → New → Activity → Empty Views Activity,填写:
- Activity Name:
MainActivity - 勾选 Launcher Activity
将 activity_main.xml 的布局代码替换成以下内容:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:padding="24dp">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="MainActivity" android:textSize="20sp" android:layout_marginBottom="24dp"/>
<Button android:id="@+id/btnToSecond" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="跳转到 SecondActivity" />
</LinearLayout>修改 MainActivity.kt,加入跳转和日志打点:
package cn.edu.sziit.android.launchmodedemo
import android.content.Intentimport android.os.Bundleimport android.util.Logimport androidx.appcompat.app.AppCompatActivityimport cn.edu.sziit.android.launchmodedemo.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets }
Log.d("LaunchMode", "MainActivity onCreate hash=${hashCode()}") binding.btnToSecond.setOnClickListener { startActivity(Intent(this, SecondActivity::class.java)) } }
// 实现 onNewIntent 方法,用于 singleTop 和 singleTask 模式的验证 override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) Log.d("LaunchMode", "MainActivity onNewIntent hash=${hashCode()}") }}3.2 创建 SecondActivity
同理新建 SecondActivity(不勾选 Launcher Activity)。
修改 activity_second.xml,内容如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:padding="24dp">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="SecondActivity" android:textSize="20sp" android:layout_marginBottom="24dp"/>
<Button android:id="@+id/btnToMain" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="返回 MainActivity" />
</LinearLayout>修改 SecondActivity.kt,加入日志打点和跳转逻辑:
package cn.edu.sziit.android.launchmodedemo
import android.content.Intentimport android.os.Bundleimport android.util.Logimport androidx.appcompat.app.AppCompatActivityimport cn.edu.sziit.android.launchmodedemo.databinding.ActivitySecondBinding
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge()
binding = ActivitySecondBinding.inflate(layoutInflater) setContentView(binding.root) setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets }
Log.d("LaunchMode", "SecondActivity onCreate hash=${hashCode()}") binding.btnToMain.setOnClickListener { startActivity(Intent(this, MainActivity::class.java)) } }
override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) Log.d("LaunchMode", "SecondActivity onNewIntent hash=${hashCode()}") }
override fun onDestroy() { super.onDestroy() Log.d("LaunchMode", "SecondActivity onDestroy hash=${hashCode()}") }}3.3 确认 AndroidManifest.xml(不加 launchMode)
此时两个 Activity 的注册如下(launchMode 留空,使用默认值 standard):
<manifest ...>
<application ...> ... <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> </activity> <activity android:name=".SecondActivity" android:exported="false" /> </application>
</manifest>3.4 运行并观察 standard 行为
运行 App,按 Main → Second → Main → Second 的顺序依次点击跳转按钮。
打开 Logcat,在搜索框输入 LaunchMode 过滤,观察日志:
如需同时查看此时的任务栈结构,可在 Android Studio 内置 Terminal 中执行:
adb shell dumpsys activity activities | findstr /R /C:"^ \* Task.*launchmodedemo" /C:"Hist.*launchmodedemo"任务栈结构如下,此时按 Back 需要 4 次才能退出 App。
记录:将你观察到的 hash 值变化情况填入实验记录表。
本步骤将 MainActivity 的启动模式设置为 singleTop,通过两个场景验证其行为:① 当 MainActivity 已显示在最顶层时,再次跳转不会新建界面;② 当 MainActivity 不在最顶层时,仍会新建一个界面。
4.1 修改 AndroidManifest.xml
为 MainActivity 添加 launchMode 属性:
<manifest ...>
<application ...> ... <activity android:name=".MainActivity" android:launchMode="singleTop" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> ... </application>
</manifest>4.2 场景一:从外部跳转回 MainActivity
路径:Main → Second → Main,观察 Logcat:
如需查看此时的任务栈结构,可在 Android Studio 内置 Terminal 中执行:
adb shell dumpsys activity activities | findstr /R /C:"^ \* Task.*launchmodedemo" /C:"Hist.*launchmodedemo"结果如下:
结论:此时 MainActivity 不在栈顶(栈顶是 SecondActivity),singleTop 不起作用,行为与 standard 完全相同。
4.3 场景二:在 MainActivity 中”启动自身”
在 activity_main.xml 中额外添加一个按钮:
<LinearLayout ...>
...
<Button android:id="@+id/btnToSecond" ... />
<!-- 新增以下按钮 ↓ --> <Button android:id="@+id/btnSelf" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="再次启动 MainActivity" />
</LinearLayout>在 MainActivity.kt 的 onCreate 中加入:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { ... binding.btnToSecond.setOnClickListener { ... }
// 新增以下代码 ↓ binding.btnSelf.setOnClickListener { startActivity(Intent(this, MainActivity::class.java)) } }
...}运行后,在 MainActivity 界面连续点击”再次启动 MainActivity”三次,观察 Logcat(以下示例省去了无关信息):
2026-03-23 16:29:48.660 LaunchMode D MainActivity onCreate hash=139004283 ← 首次创建2026-03-23 16:30:00.071 LaunchMode D MainActivity onNewIntent hash=139004283 ← 复用!hash 相同2026-03-23 16:30:02.553 LaunchMode D MainActivity onNewIntent hash=139004283 ← 继续复用如需查看此时的任务栈结构,可在 Android Studio 内置 Terminal 中执行:
adb shell dumpsys activity activities | findstr /R /C:"^ \* Task.*launchmodedemo" /C:"Hist.*launchmodedemo"任务栈:
结论:当目标 Activity 已在栈顶时,singleTop 不创建新实例,而是调用 onNewIntent() 通知当前实例。
记录:将两个场景的 hash 值变化情况填入实验记录表。
本步骤将 MainActivity 的启动模式改为 singleTask,观察其特有行为:当任务栈中已存在 MainActivity 时,系统会关闭压在它上方的所有界面,直接回到原有的那个 MainActivity。
5.1 修改 AndroidManifest.xml
将 MainActivity 的启动模式改为 singleTask:
<manifest ...>
<application ...> ... <activity android:name=".MainActivity" android:launchMode="singleTop" android:launchMode="singleTask" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> ... </application>
</manifest>5.2 验证”清栈”行为
路径:Main → Second → 再次启动 Main
确认 SecondActivity 中已有”返回 MainActivity”按钮,点击时通过 Intent 直接启动 MainActivity(与之前代码相同)。
运行并观察 Logcat:
D/LaunchMode: MainActivity onCreate hash=112013270D/LaunchMode: SecondActivity onCreate hash=128848572D/LaunchMode: MainActivity onNewIntent hash=112013270 ← MainActivity 被复用,hash 相同D/LaunchMode: SecondActivity onDestroy hash=128848572 ← SecondActivity 被系统销毁!如需查看此时的任务栈结构,可在 Android Studio 内置 Terminal 中执行:
adb shell dumpsys activity activities | findstr /R /C:"^ \* Task.*launchmodedemo" /C:"Hist.*launchmodedemo"任务栈变化:
操作 任务栈(从底→顶)──────────────────────────────────────────────启动 App → [ Main(1) ]→ Second → [ Main(1) | Second(2) ]→ Main → [ Main(1) ] ← Second 被弹出销毁,Main 复用(触发 onNewIntent)记录:此时按 Back 键,App 是直接退出还是回到 SecondActivity?将答案填入实验记录表。
本步骤新建 ThirdActivity 并将其设置为 singleInstance 模式。该模式下,这个界面单独运行在一个独立任务中,不与其他界面共用,并且整个 App 中最多只存在一个。步骤末尾对四种启动模式进行汇总对比。
6.1 创建 ThirdActivity
在 app/src/main/java/cn/edu/sziit/android/launchmodedemo/ 下右键 → New → Activity → Empty Views Activity,填写:
- Activity Name:
ThirdActivity - 不勾选 Launcher Activity
修改布局文件 activity_third.xml,内容如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:padding="24dp">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="ThirdActivity" android:textSize="20sp" android:layout_marginBottom="24dp"/>
<Button android:id="@+id/btnToMain" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="跳转到 MainActivity" />
</LinearLayout>修改 ThirdActivity.kt,加入日志打点和跳转逻辑:
package cn.edu.sziit.android.launchmodedemo
import android.content.Intentimport android.os.Bundleimport android.util.Logimport androidx.appcompat.app.AppCompatActivityimport cn.edu.sziit.android.launchmodedemo.databinding.ActivityThirdBinding
class ThirdActivity : AppCompatActivity() {
private lateinit var binding: ActivityThirdBinding
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge()
binding = ActivityThirdBinding.inflate(layoutInflater) setContentView(binding.root) setContentView(R.layout.activity_third)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets }
Log.d("LaunchMode", "ThirdActivity onCreate hash=${hashCode()}") binding.btnToMain.setOnClickListener { startActivity(Intent(this, MainActivity::class.java)) } }
override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) Log.d("LaunchMode", "ThirdActivity onNewIntent hash=${hashCode()}") }
override fun onDestroy() { super.onDestroy() Log.d("LaunchMode", "ThirdActivity onDestroy hash=${hashCode()}") }}在 AndroidManifest.xml 中注册,并指定 singleInstance:
<manifest ...>
<application ...> ... <activity android:name=".MainActivity" ... /> <activity android:name=".SecondActivity" /> <!-- 将 ThirdActivity 设置为 singleInstance 模式 --> <activity android:name=".ThirdActivity" android:launchMode="singleInstance" android:exported="false"/> </application>
</manifest>同时在 SecondActivity 的布局中添加”跳转到 ThirdActivity”按钮。打开 activity_second.xml,在原有按钮下方追加一个新按钮:
<LinearLayout ...>
...
<!-- 原有按钮,保持不变 --> <Button android:id="@+id/btnToMain" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="返回 MainActivity" />
<!-- 新增以下按钮 ↓ --> <Button android:id="@+id/btnToThird" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="跳转到 ThirdActivity" />
</LinearLayout>然后在 SecondActivity.kt 的 onCreate 中加入跳转逻辑:
class SecondActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) { ... binding.btnToMain.setOnClickListener { ... }
// 新增以下代码 ↓ binding.btnToThird.setOnClickListener { startActivity(Intent(this, ThirdActivity::class.java)) } }
...}6.2 验证独立任务栈
路径:Main → Second → Third(到达 ThirdActivity 后暂停操作)
在 Android Studio 内置 Terminal(快捷键 Alt+F12)中执行:
adb shell dumpsys activity activities | findstr /R /C:"^ \* Task.*launchmodedemo" /C:"Hist.*launchmodedemo"输出示例(位于 ThirdActivity 界面时):
* TaskRecord{a1b2c3 #42 A=cn.edu.sziit.android.launchmodedemo U=0 sz=2} * Hist #1: ActivityRecord{...} .SecondActivity * Hist #0: ActivityRecord{...} .MainActivity* TaskRecord{d4e5f6 #43 A=cn.edu.sziit.android.launchmodedemo U=0 sz=1} * Hist #0: ActivityRecord{...} .ThirdActivity可以看到,此时存在两个独立的任务栈:
任务栈 A(主栈) 任务栈 B(ThirdActivity 独占)┌────────────────┐ ┌────────────────┐│ SecondActivity │ ← 栈顶 │ ThirdActivity │ ← 栈顶(当前页面)├────────────────┤ └────────────────┘│ MainActivity │└────────────────┘在 ThirdActivity 界面按 Back 键后观察 Logcat:
D/LaunchMode: ThirdActivity onDestroy ← ThirdActivity 出栈D/LaunchMode: SecondActivity onResume ← 回到任务栈 A 的栈顶(SecondActivity)!注意:Back 键并没有回到 MainActivity(手动跳转的来源),而是回到了任务栈 A 的栈顶(SecondActivity)。
6.3 四种启动模式总结对比
| 启动模式 | 新实例条件 | 复用机制 | 是否清栈 | 是否独立任务栈 |
|---|---|---|---|---|
standard | 每次都创建 | — | ❌ | ❌ |
singleTop | 不在栈顶时创建 | 在栈顶时触发 onNewIntent | ❌ | ❌ |
singleTask | 栈中不存在时创建 | 存在时触发 onNewIntent,清除其上所有页面 | ✅ | ❌(可配 taskAffinity) |
singleInstance | 从未创建时创建 | 全局唯一实例,触发 onNewIntent | — | ✅ 独占新任务栈 |
7.1 常见问题排查
问题 1:运行项目后点击按钮没有反应
先检查按钮点击事件是否已经写在对应 Activity 的 onCreate() 中。
重点检查:
- 是否已经正确初始化
binding - 是否把
setContentView(binding.root)写在了binding = xxxBinding.inflate(layoutInflater)后面 - 按钮 ID 是否与布局文件中的 ID 完全一致,例如
btnToSecond、btnToMain、btnToThird
如果按钮 ID 写错,或者 binding 对应了错误的布局文件,点击事件就不会正确绑定。
问题 2:Android Studio 提示找不到 ActivityMainBinding
这通常说明 ViewBinding 没有开启,或者 Gradle 还没有重新同步。
排查方法:
- 打开
app/build.gradle.kts,确认已经在android {}中启用 ViewBinding - 点击 Android Studio 顶部的 Sync Now 或执行一次 Gradle Sync
- 若仍未生成,尝试执行 Build → Rebuild Project
可参考以下配置:
android { ... buildFeatures { viewBinding = true }}问题 3:Logcat 中看不到 LaunchMode 日志
先确认 Logcat 顶部筛选条件是否正确。
建议这样排查:
- 在搜索框输入
LaunchMode - 确认当前选择的是正在运行的模拟器或真机
- 确认日志级别没有被过滤掉,建议选择 Verbose 或 Debug
如果代码里没有执行到 Log.d(...),也不会有任何输出。此时要回头检查按钮点击事件是否真的触发了页面跳转。
问题 4:修改了 launchMode 后,运行结果没有变化
最常见原因是 改了代码,但没有重新安装新版 App。
建议按下面顺序操作:
- 修改
AndroidManifest.xml后重新运行 App - 如果现象仍不对,先卸载模拟器中的旧版本 App,再重新运行
- 再次执行实验步骤,重新观察 Logcat 和任务栈输出
因为启动模式配置写在 AndroidManifest.xml 中,若系统中仍保留旧安装包,就可能继续沿用旧配置。
问题 5:执行 adb 命令时提示找不到命令
adb 命令需要先配置好环境变量才能在终端中使用,请参考 1.1 搭建 Android 开发环境 · 步骤 7 完成配置。
完成后重新打开终端窗口(重要:已打开的终端不会自动读取新的环境变量),并确认:
- 模拟器已经启动,或真机已通过 USB 成功连接
- 运行
adb devices能看到设备列表,再执行实验命令
问题 6:singleTop、singleTask、singleInstance 的现象看起来分不清
可以按下面的方法重新观察,避免混淆:
- 先只看 Logcat 的 hash 值是否变化
- 再看是否触发了
onNewIntent() - 最后再用 adb 命令检查任务栈中还剩几个 Activity
判断顺序建议如下:
- hash 改变:说明创建了新实例
- hash 不变且出现
onNewIntent():说明复用了旧实例 - 栈中上层页面消失:说明发生了清栈
- 出现两个
TaskRecord:说明出现了多任务栈
如果一次同时看太多现象,初学者很容易混乱。建议每次只改一种启动模式,只验证一个结论。