13.01 四大组件-Activity
Activity 是四大组件中与我们打交道最多的一个:应用的每一屏就是一个 Activity 实例,用户看到的每次"页面切换",背后都是一次 Activity 的创建、复用或销毁。本节是组件篇的核心,覆盖四个主题:生命周期全景、状态保存与恢复、启动模式与任务栈、跳转与返回结果。
# 一、生命周期:七回调与典型路径
# 1.1 全景图
Activity官方生命周期定义:

Activity生命周期的解析说明:
七个回调各司其职,成对记忆:
| 回调 | 职责 | 与谁配对 |
|---|---|---|
onCreate() | 创建:setContentView、初始化视图/数据,savedInstanceState 恢复状态 | onDestroy() |
onStart() | 可见但不可交互(被对话框半遮挡时停在这) | onStop() |
onResume() | 前台可交互,开始动画/传感器/相机预览 | onPause() |
onPause() | 失去交互焦点:必须快速轻量(下一个页面要等它执行完才显示) | onResume() |
onStop() | 完全不可见:释放重量级资源 | onRestart()(回来)或 onDestroy()(销毁) |
onRestart() | 从 Stop 态回到前台前的一次"复活钩子" | — |
onDestroy() | 最终销毁:释放一切资源 | onCreate() |
# 1.2 典型场景的生命周期路径(背下来)
启动A:A.onCreate → A.onStart → A.onResume
A 跳转 B:A.onPause → B.onCreate → B.onStart → B.onResume → A.onStop
从 B 返回 A:B.onPause → A.onRestart → A.onStart → A.onResume → B.onStop → B.onDestroy
按 Home 键:onPause → onStop(不销毁,进程保留)
从 Home 返回:onRestart → onStart → onResume
旋转屏幕:onPause → onStop → onSaveInstanceState → onDestroy → onCreate → onStart
→ onRestoreInstanceState → onResume (整条重建链)
息屏/回桌面/跳转全屏:onPause → onStop
弹对话框/透明Activity:只到 onPause(仍部分可见,不触发onStop)
两个高频细节:
- 跳转时旧页面先 onPause,新页面才 onResume——所以 onPause 里做耗时操作会让新页面"卡"在出现前(违反 10.03 主线程规则)。
- 旋转屏幕(无
configChanges声明时)会销毁重建整个 Activity,这就是"旋转后数据丢了"的根源,见第三节。
# 1.3 生命周期实战代码骨架
public class DemoActivity extends AppCompatActivity {
private static final String TAG = "DemoActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo); // 绑定布局(必须super之后)
// 初始化视图、监听器、一次性数据
findViewById(R.id.btn).setOnClickListener(v -> gotoDetail());
if (savedInstanceState != null) {
// 重建场景:恢复临时状态(见第三节)
}
}
@Override
protected void onResume() {
super.onResume();
// 开始"前台专属"的工作:传感器监听、刷新数据、相机预览
}
@Override
protected void onPause() {
super.onPause();
// 停止前台专属工作:轻量!不要做耗时保存(新页面在等它)
}
@Override
protected void onStop() {
super.onStop();
// 释放重量级资源:注销广播、停动画
}
@Override
protected void onDestroy() {
super.onDestroy();
// 最终清理:Handler清消息(10.01)、解绑Service、关闭游标
}
}
# 二、状态保存与恢复:对抗"非自愿销毁"
Activity 会因旋转屏幕、内存不足时进程被杀、深按返回后系统回收等原因销毁重建。用户视角是"我只是转了下屏幕",所以系统提供了临时状态保存机制:
public class DemoActivity extends AppCompatActivity {
private EditText mEtDraft;
// 销毁前系统回调:保存临时UI状态(必须轻量,Bundle有1MB级限制,别塞Bitmap/大列表)
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("draft", mEtDraft.getText().toString());
outState.putInt("selected_tab", 2);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
mEtDraft = findViewById(R.id.et_draft);
if (savedInstanceState != null) { // 仅重建路径非null
mEtDraft.setText(savedInstanceState.getString("draft"));
}
}
// 也可以在这里恢复(onCreate之后执行;两者选一,通常在onCreate)
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
// 系统已自动恢复部分View状态(EditText文本、ListView位置等带id的控件)
}
}
三条边界:
- 系统会自动恢复带
android:id的 View 状态(EditText 文本、CheckBox 等)——自己管理的变量才需要手动存 Bundle有约 1MB 的 TransactionLimit,塞大对象直接 TransactionTooLargeException 崩溃;大数据走持久化(09 数据存储)或 ViewModel(21 Jetpack,专为配置变更设计)savedInstanceState区分临时 UI 状态(旋转恢复,配 Bundle)与持久业务数据(进程回收也要在,落 SharedPreferences/数据库)——两类数据的消失原因不同,保存机制也不同
# 三、启动模式与任务栈
# 3.1 任务栈(Back Stack)
每个"任务"是一个 Activity 实例的后退序列:新页面入栈顶,返回键出栈顶;最近任务列表展示的就是任务。理解栈,才能理解启动模式为什么存在。
# 3.2 四种启动模式
| 模式 | 行为 | 典型场景 |
|---|---|---|
standard(默认) | 每次新建实例入栈 | 绝大多数页面 |
singleTop | 栈顶已存在则复用(走 onNewIntent()),否则新建 | 页面内"刷新自己";通知点击打开当前页 |
singleTask | 全栈单实例;再次启动时弹出其上所有实例后复用 | 应用主页、登录后主页 |
singleInstance | 独占一个任务栈,系统级单例 | 极少用(来电页、系统级入口) |
# 3.3 代码声明与 onNewIntent
<!-- manifest中声明 -->
<activity android:name=".HomeActivity" android:launchMode="singleTask" />
// singleTop/singleTask复用实例时,不走onCreate,走onNewIntent
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// 处理新意图(如通知带来的新参数刷新页面)
setIntent(intent); // 记得更新intent,后续getIntent才是新的
refreshFromIntent();
}
singleTop 复用 vs standard 叠加的经典案例:通知栏点击打开详情页(standard)——连点三条通知会叠出三个详情页;配 singleTop——栈顶复用只走 onNewIntent。同理"搜索页在搜索页里再搜索"用 singleTop 避免无限叠加。
# 3.4 Intent FLAG:运行时控制栈
launchMode 是静态声明,FLAG 是每次启动时动态指定,两者配合:
Intent intent = new Intent(this, HomeActivity.class);
// 清出目标Activity之上的全部实例再启动(等价singleTask的出栈行为)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
// 任务栈不存在时新建任务(从通知/桌面启动外部组件时的标准姿势)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
常用 FLAG:CLEAR_TOP(清上方)、NEW_TASK(新任务栈)、CLEAR_TASK(清整个任务)、SINGLE_TOP(等价 launchMode)。FLAG 优先级高于 manifest 里的 launchMode。
# 3.5 调试任务栈(必备技巧)
adb shell dumpsys activity activities | grep -A3 "Task #" # 查看任务栈内实例顺序
启动模式疑难杂症(页面为什么没复用、back 为什么回不去)用它看栈内真实序列,比猜测有效得多。
# 四、跳转与返回结果
# 4.1 基本跳转与数据携带
// 跳转 + 携带数据(显式Intent,13章Intent详讲)
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra("article_id", 101L);
intent.putExtra("from", "home");
startActivity(intent);
// 接收端
long id = getIntent().getLongExtra("article_id", -1);
String from = getIntent().getStringExtra("from");
# 4.2 返回结果:startActivityForResult 已废弃,用 ActivityResultLauncher
public class DemoActivity extends AppCompatActivity {
// ① 声明Launcher(onCreate前注册,内部注册回调)
private final ActivityResultLauncher<Intent> mPickLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> { // ③ 结果回调(主线程)
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
String picked = result.getData().getStringExtra("picked");
Toast.makeText(this, "选择了:" + picked, Toast.LENGTH_SHORT).show();
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
findViewById(R.id.btn_pick).setOnClickListener(v -> {
// ② 发起跳转
mPickLauncher.launch(new Intent(this, PickActivity.class));
});
}
}
// PickActivity:设置结果并关闭
Intent data = new Intent();
data.putExtra("picked", "选项A");
setResult(RESULT_OK, data);
finish();
对比旧 API:startActivityForResult()/onActivityResult() 已废弃——旧写法的回调与发起分离、requestCode 手工管理易错;新 API 把"发起与回调"绑定在一个对象上,类型安全且生命周期安全(页面销毁后回调不会误触发)。
# 4.3 数据传递的注意点
- Intent Extra 适合小数据;大 Bitmap/大列表传 URI 或 id,接收端自己去取(Bundle 限制同 onSaveInstanceState)
- 传自定义对象:实现
Serializable(Gson 方案)或Parcelable(性能更好,Android 推荐;可用@Parcelize注解生成——Kotlin 章详讲)
# 五、进阶:启动 Activity 到指定屏幕(DisplayId)
车载双屏(中控 + 仪表)、折叠屏、带副屏的设备需要"把页面开到指定的那块屏上"。Android 8.0(API 26)起通过 ActivityOptions.setLaunchDisplayId() 支持此能力,流程三步(枚举屏 → 构造选项 → 带选项启动):
# 1. 枚举设备上的所有屏幕
DisplayManager displayManager = (DisplayManager)
getSystemService(Context.DISPLAY_SERVICE);
Display[] displays = displayManager.getDisplays();
for (Display display : displays) {
Log.i(TAG, "displayId=" + display.getDisplayId()
+ ", name=" + display.getName()
+ ", state=" + display.getState()); // 主屏固定为 DEFAULT_DISPLAY(0)
}
# 2. 带显示屏选项启动
int targetDisplayId = 2; // 目标副屏(如车载仪表屏/客屏)
Intent intent = new Intent(this, PassengerActivity.class);
// 跨屏启动属于"任务级"操作:必须带NEW_TASK(目标不在当前任务栈)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ActivityOptions options = ActivityOptions.makeBasic()
.setLaunchDisplayId(targetDisplayId); // API 26+:指定落屏
startActivity(intent, options.toBundle());
# 3. 运行时感知所在屏幕
// Activity在哪个屏:getDisplay()(API 30+)
Display display = getDisplay();
int myDisplayId = display != null ? display.getDisplayId() : Display.DEFAULT_DISPLAY;
// 监听屏幕插拔/亮灭(副屏热插拔场景):注册DisplayListener
displayManager.registerDisplayListener(new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(int displayId) { /* 新屏接入 */ }
@Override
public void onDisplayRemoved(int displayId) {
// 本Activity所在屏被移除时,系统会销毁其上的Activity——需要在此保存状态
if (displayId == myDisplayId) { saveState(); }
}
@Override
public void onDisplayChanged(int displayId) { /* 亮灭/尺寸变化 */ }
}, null);
约束与注意事项:
- 设备与系统支持是前提:API 26+ 提供 API,但多屏实际可用性取决于 ROM(车载/定制设备通常已启用;模拟器可用开发者选项模拟第二显示屏验证)
- 必须
FLAG_ACTIVITY_NEW_TASK:跨屏启动是任务级操作,缺 FLAG 会静默回落到默认屏 - 每块屏有独立任务栈与返回队列:副屏的返回键只影响副屏的任务——第三节的启动模式与任务栈知识在这里全部适用
- 屏幕移除 = Activity 销毁:热插拔副屏(如外接显示器)时系统走 onDestroy,状态保存机制(第二节)依然适用
- 虚拟屏渲染走 Presentation:如果只是"在副屏显示一段内容"而非完整页面交互,用
Presentation(自带独立 Context 的 Dialog 子类,绑定 VirtualDisplay)更轻量;完整可交互页面才用本节的 Activity 方案 - 双屏间通信:两块屏上的 Activity 是两个实例,通信用 EventBus/共享 ViewModel/广播/绑定的 Service,与普通跨页面通信一致
# 六、常见问题与陷阱
- 旋转屏数据丢失:未做状态保存。三件套排查:
onSaveInstanceState存临时态 / ViewModel 持久业务态(21 章)/ 关键数据落库。 - onPause 里做耗时操作:新页面要等它完成才显示,表现为"点击跳转卡顿"——保存数据用异步,onPause 只做轻量收尾。
- singleTask 页面收不到新参数:复用实例不走
onCreate,必须重写onNewIntent且setIntent()。 - 透明 Activity/对话框只触发 onPause:判断"页面是否完全不可见"用 onStop 而不是 onPause——视频暂停逻辑放错位置会出现"被对话框遮挡还在播放"。
- 进程被杀后数据"消失":内存中的静态变量/单例在进程回收后全部归零;持久数据必须落盘(09 章),恢复流程用 savedInstanceState 判断"是否重建"。
- getIntent() 拿到旧数据:singleTop/singleTask 复用时未在
onNewIntent里setIntent(intent)。 - exported 崩溃:Android 12+ 带 intent-filter 的组件未显式声明
android:exported安装直接失败(13.00)。 - 副屏启动"没反应":
setLaunchDisplayId指定了不存在的 displayId 会静默失败(不崩溃也不落屏)——启动前用DisplayManager.getDisplays()校验目标屏存在;缺FLAG_ACTIVITY_NEW_TASK也会静默回落主屏。
# 结束
本节系统讲解了 Activity:生命周期七回调与成对记忆法、八条典型路径(启动/跳转/返回/Home/旋转,配全景图)、状态保存与恢复(onSaveInstanceState 的边界:临时态 vs 持久态 vs ViewModel)、四种启动模式与任务栈(配栈示意图,singleTop/singleTask 的 onNewIntent、FLAG 运行时控制、dumpsys 调试),以及跳转与返回结果的新旧 API(ActivityResultLauncher)、多屏启动到指定 DisplayId(车载/折叠屏场景)。Activity 的生命周期是"系统托管组件"思想的集中体现,也是 Service/Broadcast 理解的基础——下一节:13.02 Service。更多内容请参考:Activity 生命周期 (opens new window)。