尧图精选

Android 11 Recents深度解析:任务管理的前台界面与底层原理

🕒 发布时间:2026/10/1 4:55:21 📁 来源:尧图网络
做了这么多年SystemUI相关的工作我经常看到有人把Android的最近任务直接叫做“最近使用的App列表”。这句话说对了一半。从界面效果看它确实是一排按时间倒序排列的应用缩略图但往深了看Android 11的Recents真正管理的是Task——也就是任务栈。Recents背后牵扯到ActivityManager、WindowManagerService、SurfaceFlinger、SystemUI、Launcher等多模块联动任何一个环节出了问题表现出来就是卡片黑屏、列表空白、顺序错乱、缩略图不更新这类疑难杂症。今天这篇文章我就按平时排查问题、阅读源码、做系统定制的顺序把Android 11的Recents彻底拆一遍。从手势入口讲到缩略图生成从排序规则聊到内存清理的误区最后给出一套实际可用的调试命令和踩坑记录。不管是做ROM定制、系统应用开发还是单纯想搞懂这个功能底层原理这篇文章应该都能给你一个相对完整的答案。1. Recents不是“最近使用的应用列表”而是“任务管理的前台界面”1.1 从Android 10到Android 11Recents的宿主换了个地方很多人不知道Android 11的Recents并不是独立App也不是Launcher里的一个页面。在Android 9及之前最近任务功能主要挂在Launcher3里也就是那个著名的Overview界面。从Android 10开始Google把整个多任务相关代码从Launcher中拆了出来逐步往SystemUI进程收拢。到了Android 11Recents的核心实现已经整体迁移到了SystemUI内部的一个模块里这个模块在源码里叫WM Shell也就是WindowManager Shell。这个迁移不是简单的搬代码。原来Launcher里的Overview是一个普通的App窗口它启动的时候还是要走Activity的生命周期而WM Shell里的RecentsView是SystemUI进程直接创建的View它不走Activity那套流程而是直接和WindowManagerService交互通过TaskOrganizer监听任务变化。带来的好处是Recents的启动速度更快动画衔接也更顺滑但代价是Debug门槛变高了——你没法像以前那样直接在Activity里打日志得从SystemUI进程去追。所以这里要建立一个基本认识Android 11的Recents本质上是SystemUI进程内WM Shell模块对外展示的一套Task管理界面。用户看到的横向卡片列表只是表象底层是WindowManager、ActivityTaskManager、SurfaceFlinger三者的协同。1.2 Recents到底管了哪些事切换、分屏、固定、移除抛开代码谈功能都是耍流氓。Recents在Android 11上交互上已经收了不少口子但核心职责还是有四块任务切换点击某个卡片把对应的Task从后台拉回前台恢复其Activity栈状态。移除任务上滑某个卡片或者点击底部的“全部清除”把这个Task从Recent列表里移除。分屏入口在卡片上长按弹出菜单选择“分屏”将当前任务和另一个任务并列显示了。Android 11的分屏在平板上用起来才比较顺手手机上其实已经很边缘了。固定屏幕也是长按菜单里的功能启用后这个App被锁定在当前界面上要退出固定模式需要同时按住返回键和最近任务键不同导航模式不同。这功能和Recents的UI交互关系不大但入口就挂在Task上。这四件事背后都离不开Task。所以理解Recents之前必须先理解Task是什么。简单说Task是一组Activity的集合以栈的形式存在栈顶Activity就是用户当前看到的界面。每个Task有自己的标识也就是TaskId还有一个归属的进程。Recents列表里的每一张卡片对应的就是一个Task的“门面”。你杀掉的其实不是Task你杀掉的是进程你移除Task进程不一定被杀死。这个重要区别后面专门讲。2. 一次“上滑悬停”手势背后有哪几个模块在联动2.1 手势输入与启动动画的边界Android 11上打开Recents最常用的方式是手势导航里的从底部上滑并悬停。表面上看就是一个手势实际上从触摸事件到Recents界面出现中间跨了三次跨进程通信。先看输入侧手指在屏幕底部上滑触摸事件先到SystemUI里的手势导航区域也就是NavigationBar那套逻辑。SystemUI在确认你的手势不是取消而是“上滑悬停”之后会去调用RecentsAnimationController。这个Controller是Recents动画的调度中枢它负责协调WindowManager和各个App窗口。动画的启动逻辑是这样的SystemUI通过bindRecentsAnimation向WindowManagerService注册一个IRecentsAnimationRunner。注册成功后WMS会暂停当前所有可见App窗口的动画状态然后以系统UI的Surface为基准创建一套新的窗口动画让每个App的Task表面跟随手势移动、缩小最终停到卡片位置。注意这里一个非常关键的点这个动画过程不是SystemUI一个人在演戏而是WMS拿到控制权后通过SurfaceControl.Transaction来操作每个窗口对应的Surface。2.2 数据从ActivityTaskManager到RecentTasksController的路径动画在播放的同时Recents里的列表数据也在同步准备。Android 11里所有Recent任务数据的源头是ActivityTaskManager也就是ActivityManagerService拆分出来的活动任务管理服务。Recents列表的数据源本质上就是ATMS里的mRecentTasks这个列表。SystemUI进程里的RecentTasksController会向ATMS注册回调监听任务变化事件。ATMS内部维护了所有Task的LRU链表按lastActiveTime字段排序。每当有Task从后台回到前台或者有新的Task创建ATMS都会更新这个时间戳并通知SystemUI。RecentTasksController再把原始Task数据转换成RecentTaskInfo这种带展示信息的结构分发给RecentsView。这里有一个很容易踩坑的细节ATMS返回的数据里包含的Task并不全是用户可以交互的。有一些系统内部Task会被excludeFromRecents标记过滤掉还有一些特殊的“home task”也不能出现在列表里。Android 11上还在RecentTasksController里做了loadRecentTasks的异步加载避免在UI线程去跨进程查询否则上滑手势那一下子就会有可感知的卡顿。2.3 RecentsView如何把Task变成卡片数据到了RecentsView之后真正的渲染工作才开始。RecentsView本身是WM Shell里的一个容器View它会为每一个Task创建一个对应的TaskView。每个TaskView内部包含三块主要区域缩略图区域、应用图标区域、应用名称区域。缩略图区域是根据Task快照生成的TaskSnapshotSurface应用图标和名称则是从PackageManager里拿的。这里涉及到一个很重要的复用逻辑你以为上滑一次就创建了十几个TaskViewAndroid 11并不会这么干。RecentsView本身是一个类似RecyclerView的复用容器它只inflate当前屏幕可见区域加上缓存区所需要的卡片数量。左右滑动的时候旧的卡片View会被回收新的卡片按需创建。所以排查Recents相关卡顿问题时不能只盯着View的创建逻辑还要关注数据列表大小和快照获取速度——很多时候卡的原因是列表拉取太慢或者快照没命中而不是View本身。3. 缩略图不是“截屏”而是TaskSnapshot的功劳3.1 Snapshot在哪些时机被捕获Recents卡片上那张界面缩略图很多人以为是从SurfaceFlinger截屏截出来的。实际在设计上是“已知快照”更准确的说法叫TaskSnapshot它由TaskSnapshotController在特定时机生成并存进缓存。这个时机不是随机的而是在Activity进入停止状态前后。具体来说当一个Activity因为被其他Activity完全覆盖而不可见时ActivityRecord会走到Stopped状态。在Android 11的TaskSnapshotController逻辑里系统会检查这个Task是否值得生成快照——应用是否允许截屏没有FLAG_SECURE、窗口是否是硬件加速渲染的、Activity是不是透明的这些条件全部满足之后才会通过Snapshotter抓取当前帧的Buffer生成一个包含位图和元数据的TaskSnapshot。你可以把它理解成“拍证件照”的流程系统不是随时按快门而是在你离场时抓拍一张标准照存好待用。这张照片会关联到TaskId上下次Recents打开时直接通过TaskId去取快照比临时截屏快得多。3.2 为什么有些应用在Recents里是空白页这是个出现频率很高的问题。银行App、视频播放器这类应用你在Recents里通常会看到两种异常一种是缩略图是一张纯白的背景板另一种是缩略图干脆显示应用图标。这两种情况的原因完全不同。纯白背景板一般是该App窗口设置了FLAG_SECURE。这个Flag的语义是禁止在当前安全窗口不可见时截取内容同时也会让系统无法生成合法快照。Android 11会对这类Task生成一张“受保护快照”用Activity的主题色做背景填充。所以你会看到一张干净的白底或者黑底卡片而不是应用界面。显示应用图标的情况则是快照真的没有命中也没有生成。常见原因是Task还处于启动初期比如应用从冷启动到出现第一帧之间系统找不到可捕获的窗口内容或者该Task是一个纯透明Activity没有可见窗口内容可以截取。3.3 缓存与磁盘快照的存续周期TaskSnapshot不是每次打开Recents都现场生成的它有两级缓存。第一级是内存缓存也就是SnapshotCache以TaskId为Key保存最近使用的快照内存不够时会按LRU淘汰。第二级是磁盘缓存位于/data/system_ce/0/snapshots/目录下每个快照是一个文件。有个设计值得注意Android 11的快照文件是以用户ID和TaskId等维度组织的每次进程重启后磁盘缓存还能用所以杀掉SystemUI进程再重启Recents里的缩略图并不会全部消失因为可以从磁盘恢复。但磁盘缓存会被一些事件触发清理比如用户执行“全部清除”、某个Task对应的包被卸载系统都会主动删除对应快照文件避免展示出僵尸应用的界面。实际操作中遇到缩略图不更新的情况很多是这个缓存机制搞的鬼。最常见的就是某个App升级了版本界面上的一级页面换了主题色但Recents里的缩略图还是老样子甚至点进去再退出来缩略图依然是旧的。这种情况多半是快照的生成时机没赶上Task在可见状态下没有重新走Stopped的流程于是老快照一直有效。4. 桌面上看到的卡片尺寸、排序、分组与交互背后4.1 卡片尺寸的计算方式与横竖屏差异Android 11的Recents卡片在手机上是横向滑动的列表每张卡片几乎占满屏幕宽度只有左右两侧露出一点点边缘做视觉提示。这个尺寸不是写死的而是由RecentsViewConfiguration按屏幕宽高、density、横竖屏状态动态计算出来的。计算的核心逻辑是竖屏下卡片宽度取屏幕宽度的某个百分比高度按屏幕可用高度的比例缩小横屏下卡片会变得更矮更宽保证列表在横向滑动时用户还可以看清卡片的纵向内容。在平板上Android 11会把卡片限制在一个最大宽度内避免卡片在横屏下拉伸得过大看起来像桌面。这里给一个经验如果你做定制想要调整卡片尺寸不要去改CardView的LayoutParam那些值会被运行时覆盖。真正该改的是RecentsViewConfiguration里的比例计算逻辑改完重建SystemUI进程才能生效。我早期做定制时在这个坑里浪费了整整一天改XML布局怎么都不生效后来发现WM Shell在运行时用代码覆盖了布局参数。4.2 排序规则lastActiveTime的更新时机Recents列表的顺序是“最近使用的放最前面”这个“最前”的判定依据是Task的lastActiveTime。但这个时间戳的更新时机并不完全等于“用户最后一次打开这个App”。更准确的表述是每当一个Task被带到前台时ATMS会把它的lastActiveTime戳更新为当前时间。这里的“被带到前台”可以从两个路径触发一是正常启动App二是用户从Recents中点击卡片恢复Task。但有个特殊情况要注意如果一个App本身就带有多任务特性比如通过Intent.FLAG_ACTIVITY_NEW_TASK启动了多个Task那么每个Task都有自己的时间戳它们在Recents里的位置可能和你日常认知的“App最近使用顺序”对不上。排查顺序错乱问题时我通常先确认是不是单Task场景然后再看有没有第三方App用了一些“回到桌面再进另一个界面”的花式启动方式。绝大多数排序乱不是SystemUI的问题而是App自己创建了异常多的Task。4.3 长按菜单、分屏与固定屏幕如何挂在Task上Android 11 Recents卡片的长按菜单有三项应用信息、分屏、固定。这三个入口背后分别对应着不同的系统逻辑。应用信息最直接就是弹出那个包名的详情页实际上是通过ActivityTaskManager构造一个ApplicationInfo的跳转Intent给Settings应用处理。分屏则复杂一点它会把当前选中的Task和正在展示的Recents界面本身作为两个“任务卡片”发起分屏组合操作由WMS创建分屏窗口。固定屏幕执行的是ScreenPinningRequest本质是把当前Task设置为系统级的锁定状态过程中需要用户确认。这三个操作都绕不开Task也绕不开WindowManager那边的窗口Token。你在做定制时如果发现长按菜单项点击没反应优先去查是不是当前Task的Token失效了或者这个Task已经被标记为固定、不可分屏。分屏和固定屏幕在当前系统里都是“有状态”的状态冲突了操作自然被拦下来。5. Recents与“内存清理”之间的真实关系5.1 Task不等于进程Recents列表为什么能留那么多“已死”应用这是绝大部分人的认知盲区。Recents列表中一个卡片对应一个TaskRecord而不是一个ProcessRecord。用户对一个App左滑删除任务时系统会从mRecentTasks中移除该Task并通知ActivityTaskManager停止该Task内的Activity。但进程本身归AMS管一个进程里可能跑着好几个Task也可能Task已经清空了进程还活着因为AMS还要看它有没有Service、ContentProvider、BroadcastReceiver在运行。反过来也一样。进程被lmkd杀了TaskRecord却可以安然躺在Recents列表里。所以你会看到Recents里有二十个App其中十五个可能早就没有进程了。每次打开Recents的时候这十五个App的缩略图照样正常显示因为缩略图是快照不是实时画面。这也是Recents快照机制一个很重要的设计动机——让用户以为应用还在后台等着你实际上内存早就被系统回收了。5.2 点击旧任务时系统到底在启动一个新的Activity还是恢复旧的这个问题在技术社群经常被问。答案取决于Task和Activity的状态。如果你点的是一个进程已被杀死的Task系统会重建Task里的根Activity。如果这个Task只有一个Activity那等于是冷启动一个新的Activity实例但TaskId不变ActivityRecord也是重新创建的。用户的表现就是从桌面图标冷启动没有区别不过界面上会恢复到这个Activity的onSaveInstanceState状态——当然前提是Activity正常保存了状态。如果进程还活着但Task被移到了后台那么点击卡片只是做一个moveTaskToFront的操作把整个Task连同栈里的Activity一起拉回来所有Activity实例都还在不会重建。这是最理想的恢复路径也是Recents设计的初衷。理解这一点对排查“点击卡片黑屏/闪退”这类问题很有用凡是在Recents里点击之后闪退多半是App自身的保存状态恢复逻辑有bug因为它走得是完整的重建流程不是纯粹前台切换。5.3 被“清除全部”后后台任务真的被杀了么结论是不一定。Android 11的“全部清除”执行的是removeAllVisibleRecentTasks它做的事情是把当前Recents里所有可见Task从Recent列表里移除并通知ActivityTaskManager去finish每个Task里的Activity。如果一个App被从全部清除列表中移除但它的进程里还驻留有Service或者它被系统划分为“已缓存”进程但没有真正触发低内存回收那么进程依然在后台跑着。很多App做得比较激进通过前台Service和startForeground来保活用户清掉Recents之后App进程并不会消失因为它宣称自己正在提供服务。Android对这种“被清掉还在跑”的App更倾向于给一个通知而不是强制杀进程。你现在再回看一些“清后台加速”类的App它们最大的问题是混淆了removeTask和forceStopProcess的区别。只有调用了forceStopPackage或killProcess才能指定进程“清除后台”优化在Android 11上实际的作用很有限。6. 改ROM/做系统应用时Recents相关的调试与避坑记录6.1 常用的几条命令先把状态看清楚Recents问题不好排查因为它的运行进程是SystemUI界面又是直接挂在窗口管理器上的普通的App调试日志很难覆盖。我从Android 11时代开始就固定用下面这组命令每次遇到Recents相关问题先跑一遍# 查看ActivityTaskManager里的Recents相关状态 adb shell dumpsys activity recents # 查看当前所有Task和Activity栈的完整状态 adb shell dumpsys activity activities # 查看系统里所有窗口和窗口Token信息 adb shell dumpsys window windows # 查看SystemUI进程是不是活着PSS占用情况 adb shell dumpsys meminfo com.android.systemuidumpsys activity recents能直接看到RecentTask列表包括每个Task的ID、名称、是否被排除显示、最近活跃时间、以及关联的快照是否有效。dumpsys activity activities则能看到每个Task里的ActivityRecord确认指定任务的前台/后台状态。两个命令配合使用基本能把“Recents列表数据和实际窗口状态对不上”的问题定位到具体环节。6.2 我遇到过的几个典型问题与排查链路先说一个非常经典的问题Recents列表空白但系统没有崩溃日志里也没有明显的异常。我当时排查的顺序是先dumpsys activity recents看数据列表结果发现列表里确实有Task说明数据源没问题再去看SystemUI进程是否因为快照加载失败把整个RecentsView吞掉了发现日志里有TaskSnapshot加载超时最后定位到是设备存储空间满了磁盘快照写入失败导致Controller拿不到快照初始化卡片。所以如果你也遇到Recents一片空白先看存储空间再看日志里有没有快照相关错误。另一个常见问题是横向滑动卡片时明显掉帧。Recents数据量少的时候很流畅一旦展开到二三十个卡片就开始卡。原因是每次滑动到新的卡片都要去读磁盘快照而磁盘IO在低端机上比较慢。Android 11的优化思路是提前异步加载当前卡片周围几个位置的快照数据避免滑动到跟前时才去读盘。如果你的定制ROM把Recents列表的最大数量调大了比如从默认的固定上限改成不限制那低端机上大概率出现这种卡顿建议先确认一下系统里的ro.lockdisable.recents.max.task.count相关配置或者干脆回到默认数量限制。还有一种情况是从Recents点击卡片回到App时动画黑屏一闪或者卡在动画结束位置点不了。这类问题多半出在起始窗口和Task快照的衔接上。Activity启动时会创建一个“starting window”也就是我们说的StartingWindow如果Recents动画结束时StartingWindow已经先一步销毁而应用自己的界面还没画出来屏幕就会短暂黑掉。解决办法通常是检查Activity是否禁用了windowDisablePreview或者把启动窗口的主题背景色设置成和应用主色调一致视觉上就不那么突兀了。6.3 多屏/投屏场景下Recents的另类行为Android 11对多显示器的支持比之前完善了很多如果你做的是车机、平板或者远程投屏相关的定制也会和Recents产生交集。一个容易忽略的地方是不同Display上可以有各自的活动窗口但Recents列表在Android 11里主要是以默认Display为中心的也就是Display.DEFAULT_DISPLAY。在实际测试中同一个Task如果从默认屏被转移到副屏它的Task快照依然存在但Recents列表的显示顺序可能不会实时更新因为ATMS里的Recents数据只关心任务最近活跃时间不管它在哪个Display上活跃。另一个头疼的问题是当副屏播放了带有FLAG_SECURE的视频内容时默认屏的Recents里那张卡片可能是正常的但一旦用户把代表副屏窗口的Task切回默认屏缩略图可能就会因为安全窗口约束变成空白。这种跨Display的快照安全问题在投屏场景下尤其突出。如果你做的方案里涉及USB OTG外接显示器或者无线投屏我的建议是先去确认该Display上的窗口是不是都允许被快照捕获尤其是一些系统级播放器窗口。必要时可以在定制层面对特定Display的Task单独做快照白名单管理不过这就属于比较细的私有方案了。写在最后Android 11的Recents给我的最大感受是它表面上是一个UI功能实际上已经变成了系统窗口、任务、进程、快照四套体系的一个交汇点。想要真正弄懂它不需要把每个类都背下来但至少要建立一张清晰的链路图——从手势到动画、从ATMS到RecentTasksController、从TaskSnapshot到TaskView每一环都搞清楚它负责什么排查问题的时候就不会两眼一抹黑。最后分享一个我在实际调试中的小技巧很多Recents相关的问题在真机上很难复现但在模拟器上又跑不出同样的表现。这种情况下可以考虑先用adb shell settings put global animator_duration_scale 0关掉系统动画让Recents的入场和退场都平铺直叙地展示往往能更快暴露出卡片的真实状态到底是快照缺失、数据顺序不对还是窗口层级错了一眼就能看出来。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →