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 中启动 Activity
  • FLAG_ACTIVITY_CLEAR_TOP:如果目标 Activity 已在栈中,清除它之上的所有 Activity
  • FLAG_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 的栈操作有个整体认识。

Activity 栈管理核心流程 startActivity 调用 AMS 解析 Intent & Flag 查找/创建 TaskRecord 执行栈操作(push/pop/clear/reorder) 创建/复用 ActivityRecord 生命周期调度 检查 launchMode 检查 Intent Flag 根据 taskAffinity 决定复用还是新建 按规则调整栈内顺序 复用或新建实例

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 生命周期、进程管理,都会轻松很多。


公众号:蓝海资料掘金营,微信deep3321