13.01 四大组件-Activity

2025/6/14 Android 开发基础

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

# 一、生命周期:七回调与典型路径

# 1.1 全景图

Activity官方生命周期定义:

Activity生命周期图

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)

两个高频细节:

  1. 跳转时旧页面先 onPause,新页面才 onResume——所以 onPause 里做耗时操作会让新页面"卡"在出现前(违反 10.03 主线程规则)。
  2. 旋转屏幕(无 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的控件)
    }
}

三条边界:

  1. 系统会自动恢复带 android:id 的 View 状态(EditText 文本、CheckBox 等)——自己管理的变量才需要手动存
  2. Bundle 有约 1MB 的 TransactionLimit,塞大对象直接 TransactionTooLargeException 崩溃;大数据走持久化(09 数据存储)或 ViewModel(21 Jetpack,专为配置变更设计)
  3. 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() 支持此能力,流程三步(枚举屏 → 构造选项 → 带选项启动):

启动Activity到指定Display示意图

# 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);

约束与注意事项:

  1. 设备与系统支持是前提:API 26+ 提供 API,但多屏实际可用性取决于 ROM(车载/定制设备通常已启用;模拟器可用开发者选项模拟第二显示屏验证)
  2. 必须 FLAG_ACTIVITY_NEW_TASK:跨屏启动是任务级操作,缺 FLAG 会静默回落到默认屏
  3. 每块屏有独立任务栈与返回队列:副屏的返回键只影响副屏的任务——第三节的启动模式与任务栈知识在这里全部适用
  4. 屏幕移除 = Activity 销毁:热插拔副屏(如外接显示器)时系统走 onDestroy,状态保存机制(第二节)依然适用
  5. 虚拟屏渲染走 Presentation:如果只是"在副屏显示一段内容"而非完整页面交互,用 Presentation(自带独立 Context 的 Dialog 子类,绑定 VirtualDisplay)更轻量;完整可交互页面才用本节的 Activity 方案
  6. 双屏间通信:两块屏上的 Activity 是两个实例,通信用 EventBus/共享 ViewModel/广播/绑定的 Service,与普通跨页面通信一致

# 六、常见问题与陷阱

  1. 旋转屏数据丢失:未做状态保存。三件套排查:onSaveInstanceState 存临时态 / ViewModel 持久业务态(21 章)/ 关键数据落库。
  2. onPause 里做耗时操作:新页面要等它完成才显示,表现为"点击跳转卡顿"——保存数据用异步,onPause 只做轻量收尾。
  3. singleTask 页面收不到新参数:复用实例不走 onCreate,必须重写 onNewIntent 且 setIntent()。
  4. 透明 Activity/对话框只触发 onPause:判断"页面是否完全不可见"用 onStop 而不是 onPause——视频暂停逻辑放错位置会出现"被对话框遮挡还在播放"。
  5. 进程被杀后数据"消失":内存中的静态变量/单例在进程回收后全部归零;持久数据必须落盘(09 章),恢复流程用 savedInstanceState 判断"是否重建"。
  6. getIntent() 拿到旧数据:singleTop/singleTask 复用时未在 onNewIntent 里 setIntent(intent)。
  7. exported 崩溃:Android 12+ 带 intent-filter 的组件未显式声明 android:exported 安装直接失败(13.00)。
  8. 副屏启动"没反应":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)。