Recent Tasks 管理:最近任务列表的维护、清除、锁定机制
最近任务列表,说白了就是咱们按那个「方块键」看到的界面。这个功能看似简单,但坑特别多。我当年刚接手系统UI时,就被各种奇怪的场景折磨过——应用明明退出了,列表里还挂着;用户想锁定某个任务,结果重启手机就丢了。今天咱们就把这块彻底讲透。
Recent Tasks 的数据结构
先看看系统怎么存这些任务。核心数据结构是 RecentTasks 类,它维护了一个有序列表:
// frameworks/base/services/core/java/com/android/server/am/RecentTasks.java
class RecentTasks {
// 核心列表,按最近使用排序
private final ArrayList<TaskRecord> mTasks = new ArrayList<>();
// 最大数量,不同设备不一样
private int mMaxRecentTasks = 48; // 手机默认
// 锁定的任务集合
private final SparseArray<TaskRecord> mLockedTasks = new SparseArray<>();
}
这里有个细节:mTasks 里的顺序就是用户看到的顺序。最新的任务插在头部,最旧的挤到尾部。当数量超过 mMaxRecentTasks 时,尾部任务会被移除。
核心要点:RecentTasks 不是简单的 FIFO 队列。它要考虑任务分组、锁定状态、Activity 类型等多种因素。我见过不少第三方 ROM 改这块改出 bug,就是因为只考虑了「先进先出」。
任务列表的维护机制
任务什么时候加入列表?什么时候移除?咱们一步步看。
加入时机
系统在 ActivityManagerService 的 startActivity 流程中,会调用 addRecentTaskLocked:
// 简化后的逻辑
void addRecentTaskLocked(TaskRecord task) {
// 1. 检查是否应该加入
if (!shouldAddToRecentTasks(task)) {
return;
}
// 2. 移除旧的相同任务
removeTaskByIntent(task.intent, task.userId);
// 3. 插入到头部
mTasks.add(0, task);
// 4. 裁剪超出部分
trimRecentTasks();
}
嗯,这里要注意 shouldAddToRecentTasks 的判断逻辑。我记得有一次排查问题,发现某个应用怎么都不出现在最近任务里。查了半天,原来是它的 Activity 设置了 excludeFromRecents=true:
<activity
android:name=".SecretActivity"
android:excludeFromRecents="true" />
这个属性一设,系统直接跳过,根本不会加入列表。
移除时机
任务移除有几种情况:
- 用户手动清除:滑动删除或点「清除全部」
- 数量超限:最旧的任务被挤出
- 应用被卸载:所有相关任务全部移除
- 系统内存不足:低内存杀进程时,任务记录也会清理
我曾经遇到一个线上问题:用户反馈「清除全部」后,某些应用还在列表里。后来发现是 removeAllTasks 方法里忘了处理锁定任务:
void removeAllTasks() {
for (int i = mTasks.size() - 1; i >= 0; i--) {
TaskRecord task = mTasks.get(i);
// 坑:锁定任务不应该被清除
if (task.isLocked()) {
continue; // 跳过锁定任务
}
removeTaskAtIndex(i);
}
}
注意:Android 10 之后,「清除全部」的行为变了。它不会清除锁定任务,也不会清除前台服务的任务。这个改动让很多用户困惑,但其实是合理的——你正在听音乐,不小心点了清除全部,音乐不应该停。
任务锁定机制
锁定功能是 Android 9 引入的。用户可以在最近任务列表里,点击任务卡片上的锁图标,把它锁定。
锁定的本质
锁定其实就是在 TaskRecord 上设置一个标志位:
// frameworks/base/services/core/java/com/android/server/am/TaskRecord.java
class TaskRecord {
boolean mIsLocked; // 锁定标志
void setLocked(boolean locked) {
mIsLocked = locked;
// 持久化到磁盘
persistLockedState();
}
}
这个状态会持久化到 task_persistent_data.xml 文件中。所以重启手机后,锁定状态还在。
锁定后的行为变化
锁定任务有几个特殊行为:
| 操作 | 未锁定 | 已锁定 |
|---|---|---|
| 滑动清除 | 移除任务 | 无反应(或提示已锁定) |
| 清除全部 | 移除所有 | 保留锁定任务 |
| 数量超限裁剪 | 移除最旧 | 跳过锁定任务 |
| 应用卸载 | 移除相关任务 | 移除(锁定不防卸载) |
你想想看,锁定其实只防「误清除」,不防「应用被卸载」。这个设计很合理——你锁定了微信,但你把微信卸载了,任务留着也没意义。
核心流程图
下面这张图展示了最近任务从创建到销毁的完整生命周期:
清除策略的演进
不同 Android 版本的清除策略不太一样。我整理了一下:
| 版本 | 清除全部行为 | 锁定支持 |
|---|---|---|
| Android 5.x | 清除所有任务,包括前台服务 | 不支持 |
| Android 6-8 | 清除所有,但保留前台服务 | 不支持 |
| Android 9 | 清除所有,保留前台服务和锁定任务 | 支持 |
| Android 10+ | 同上,但增加了更多豁免条件 | 支持,持久化到磁盘 |
为什么会这样演进?说白了就是用户体验的不断优化。早期版本「一刀切」的做法,经常导致音乐播放器、下载任务被意外中断。后来 Google 学聪明了,加了各种豁免条件。
避坑指南:如果你在开发系统应用,需要监听「清除全部」事件,别用 onTaskRemoved。这个回调在普通清除时能触发,但「清除全部」时可能不会。我曾经被这个坑过,后来改用 UsageStatsManager 的监听才搞定。
锁定机制的实现细节
锁定功能在 UI 层和 Framework 层都有实现。UI 层负责显示锁图标和处理点击事件,Framework 层负责存储状态和执行策略。
持久化存储
锁定状态存在 /data/system/task_persistent_data.xml 里:
<task-persistent-data>
<task id="42" locked="true" />
<task id="56" locked="false" />
<task id="78" locked="true" />
</task-persistent-data>
系统启动时,RecentTasks 会读取这个文件,恢复锁定状态。嗯,这里有个细节:如果某个任务对应的应用被卸载了,这个记录就成了「孤儿」。系统在下次启动时会自动清理这些无效记录。
锁定与分屏的交互
锁定任务在分屏模式下有个特殊行为:如果两个任务中有一个被锁定,分屏退出时,锁定的任务会保留,另一个会被清除。这个逻辑在 ActivityManagerService 的 removeStacksInWindowingModes 方法里:
void removeStacksInWindowingModes(int windowingMode) {
for (int i = mStacks.size() - 1; i >= 0; i--) {
ActivityStack stack = mStacks.get(i);
if (stack.getWindowingMode() == windowingMode) {
TaskRecord topTask = stack.getTopTask();
// 锁定任务不参与自动清除
if (topTask != null && topTask.isLocked()) {
continue;
}
stack.finishAllActivities();
}
}
}
我记得有一次测试分屏功能,发现退出分屏后两个应用都消失了。查了半天,原来是锁定标志没传对——UI 层点了锁定,但 Framework 层没收到回调。这种跨进程通信的 bug 最难排查。
性能优化建议
最近任务列表如果维护不当,会影响系统性能。我给出几个建议:
- 控制列表大小:
mMaxRecentTasks不要设太大。手机 48 个够用,平板可以适当增加到 64 个。 - 延迟加载缩略图:最近任务的截图不要一次性加载完。用户滑动时再按需加载,能省不少内存。
- 批量操作加锁:清除全部时,用
batchUpdate模式,避免频繁通知 UI 刷新。
总结一下:Recent Tasks 管理看似简单,但涉及任务生命周期、用户交互、持久化存储等多个方面。锁定机制是 Android 9 之后的重要改进,它给了用户更多控制权。理解这些细节,能帮你写出更稳定的系统应用。
公众号:蓝海资料掘金营,微信deep3321