3、Activity 栈管理:Task、ActivityRecord、TaskRecord、返回栈(Back Stack)机制
好,咱们今天来聊聊 Activity 的栈管理。说实话,这块内容是整个 Activity 系统的核心骨架。你想想看,用户点开一个 App,然后跳转、返回、再跳转,这些操作背后是谁在记录?谁在管理?就是栈。
我刚开始啃这部分源码的时候,也被 TaskRecord、ActivityRecord 这些概念绕得头晕。后来我养成了一个习惯:把 Task 想象成一个任务卡片,ActivityRecord 就是卡片上的一个个便签。这么一想,整个逻辑就清晰多了。
3.1 核心数据结构:ActivityRecord 与 TaskRecord
咱们先看两个最基础的对象。
3.1.1 ActivityRecord
ActivityRecord,说白了就是 Activity 在 AMS 中的“代言人”。每个 Activity 实例,在 AMS 里都有一个对应的 ActivityRecord 对象。它记录了 Activity 的所有状态信息。
核心字段一览:
state:当前生命周期状态(INITIALIZING、RESUMED、PAUSED 等)task:所属的 TaskRecord 引用appToken:与 WMS 通信的令牌launchMode:启动模式(standard、singleTop、singleTask、singleInstance)intent&info:启动 Intent 和 ActivityInfo
我记得有一次排查一个奇怪的闪退问题,日志里一直报 ActivityRecord 的 state 异常。后来发现是某个三方 SDK 在 onPause 里又启动了一个 Activity,导致状态机乱掉了。嗯,这里要提醒大家:不要在生命周期回调里做复杂的启动操作。
3.1.2 TaskRecord
TaskRecord 代表一个“任务”。你可以把它理解成一个栈容器,里面按顺序存放着多个 ActivityRecord。每个 TaskRecord 都有一个唯一的 taskId,还有一个 mActivities 列表,就是那个经典的 ArrayList。
// frameworks/base/services/core/java/com/android/server/wm/TaskRecord.java
class TaskRecord {
int taskId; // 任务唯一 ID
ArrayList<ActivityRecord> mActivities; // 栈内 Activity 列表
ActivityRecord mRoot; // 栈底 Activity
int mStackId; // 所属栈 ID
boolean mIsPersistable; // 是否可持久化
String affinity; // 任务亲和性
}
这里有个关键点:一个 TaskRecord 里的 Activity 是按后进先出的顺序管理的。你每次 startActivity,新的 ActivityRecord 就会被 push 到栈顶。按返回键,就 pop 掉栈顶。
3.2 返回栈(Back Stack)机制
返回栈,其实就是 TaskRecord 内部那个 mActivities 列表的“行为表现”。用户按返回键时,系统会从栈顶开始逐个销毁 Activity。
为什么会这样设计?说白了,就是为了符合用户直觉。你打开一个页面,再打开一个,再打开一个……返回的时候,自然希望原路返回。这就是栈的天然优势。
我个人习惯:在调试返回栈问题时,用 adb shell dumpsys activity activities 命令查看当前所有 Task 和 ActivityRecord 的状态。这个命令输出的信息非常详细,包括每个 Activity 的 state、taskId、栈位置等。
3.3 栈管理核心逻辑
咱们来看几个关键场景。这些场景我在项目中都遇到过,踩过坑,也填过坑。
3.3.1 启动模式对栈的影响
| 启动模式 | 行为描述 | 典型场景 |
|---|---|---|
| standard | 每次都创建新实例,压入当前 Task 栈顶 | 普通页面跳转 |
| singleTop | 如果栈顶已是该 Activity,复用;否则创建新实例 | 通知栏点击跳转 |
| singleTask | 在 Task 内查找是否存在该 Activity,存在则将其上方的 Activity 全部出栈 | 主页面、首页 |
| singleInstance | 该 Activity 独占一个 Task | 来电界面、系统级弹窗 |
我曾经遇到过一个 bug:用户从 A 页面跳到 B,B 跳到 C,C 再跳回 A 时,A 被创建了两次。查了半天,发现是 A 的 launchMode 设成了 standard,而 B 和 C 都是 singleTask。结果每次从 C 跳 A,系统都在当前 Task 里新建了一个 A 实例。解决方案?把 A 也改成 singleTask,或者用 FLAG_ACTIVITY_CLEAR_TOP。
3.3.2 Intent Flag 对栈的操作
除了 launchMode,Intent Flag 也是控制栈行为的重要手段。我常用的几个:
FLAG_ACTIVITY_NEW_TASK:在新 Task 中启动 ActivityFLAG_ACTIVITY_CLEAR_TOP:如果目标 Activity 已在栈中,清除它之上的所有 ActivityFLAG_ACTIVITY_SINGLE_TOP:类似 singleTop 模式FLAG_ACTIVITY_REORDER_TO_FRONT:把栈中已有的 Activity 移到栈顶
避坑指南:我曾经在项目里同时使用了 FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_CLEAR_TOP,结果发现某些机型上出现了奇怪的 Task 分裂现象。后来查源码发现,这两个 Flag 的组合在某些场景下会导致系统创建新的 TaskRecord,而不是复用已有的。所以,使用 Flag 组合时一定要在多个机型上充分测试。
3.4 栈管理的核心流程图
下面这张图是我自己梳理的,展示了从 startActivity 到最终入栈的完整流程。你看完应该能对 AMS 的栈操作有个整体认识。
3.5 多 Task 场景下的栈管理
实际项目中,一个 App 往往有多个 Task。比如你从桌面启动 App,这是一个 Task;然后你点击通知栏跳转,可能又创建了一个新 Task。这时候,AMS 需要管理多个 TaskRecord。
每个 TaskRecord 都有一个 mStackId,指向它所属的栈(Stack)。在 Android 10 之后,系统引入了“Activity Stack”的概念,用来管理一组 Task。不过底层逻辑没变:每个 Task 内部还是一个后进先出的栈结构。
关键点总结:
- ActivityRecord 是单个 Activity 的“身份证”
- TaskRecord 是 Activity 的“容器”,内部用 ArrayList 模拟栈
- 返回栈就是 TaskRecord 内 ActivityRecord 的进出顺序
- launchMode 和 Intent Flag 共同决定栈操作行为
- 多 Task 场景下,AMS 通过 taskId 和 stackId 管理
嗯,栈管理这块内容,说白了就是理解“谁在哪儿,怎么进去,怎么出来”。你把这个逻辑理清了,后面再讲 Activity 生命周期、进程管理,都会轻松很多。