做 Android 系统定制和 ROM 开发的朋友应该都有同感:很多功能从用户视角看只是“点一下”的事,真到源码层面就是跨好几个进程的大工程。最近任务(Recents)就是这类功能中最典型的一个——你在手机上从底部上滑一下,看到的那个应用卡片列表,背后至少牵扯 system_server、SystemUI、Launcher(QuickStep)、WindowManager 四个模块。尤其到了 Android 11(API 30),Recents 不再像 Android 9 及以前那样由 SystemUI 里的独立页面承载,而是被深度迁移进了 Launcher 的 QuickStep 组件里,跟手势导航、任务快照、窗口转场动画全部绑在一起。
这篇文章我以 Android 11 的 AOSP 主线源码为基准,把 Recents 从整体架构、任务数据来源、快照生成机制、手势触发与动画链路、常见故障排查这几个维度完整拆一遍。内容适合三类人看:一是做 ROM / 系统定制,需要改最近任务样式或行为的同学;二是做 Launcher 开发,想搞清楚 QuickStep 到底怎么和系统通信的同学;三是普通应用开发者,想弄明白为什么自己 App 在后台里的缩略图会黑屏、为什么在 Recents 里滑动删除会影响任务栈。全文只讲 Android 11 这一条主线,不掺别的版本的东西,遇到版本差异的地方我会专门说明。
1. 整体架构:Android 11 的 Recents 到底住在哪里
1.1 一次历史性迁移:从 SystemUI 的独立页面到 Launcher 里的 Overview
在 Android 9 及之前,Recents 的 UI 是系统 UI 进程里的一个独立 Activity,用户点“方块键”后,SystemUI 把这个页面拉起来,里面用卡片列表展示最近任务。到了 Android 10,Google 决定把 Recents 的整体视觉和交互逻辑迁移到 Launcher3 的 QuickStep 模块中,Android 11 延续并强化了这套设计。
为什么这么做?核心原因是手势导航。全面屏手势出现后,用户“从底部上滑进入最近任务”这个动作,本质上是要把当前正在运行的应用窗口平滑缩小成后台卡片,再嵌进 Launcher 的网格里。如果 Recents 的 UI 还放在 SystemUI 里,那么跨进程的窗口转场会非常别扭——应用窗口的缩小动画和后台页面展开动画之间很难做到连贯。把 Recents 的 UI 直接做成 Launcher 的一个视图(Overview),让 Launcher 同时负责桌面和后台页,动画就能在一个进程内协调,视觉上顺滑得多。
所以在 Android 11 上,你会看到这些关键变化:
- Recents 的容器 UI 放在
packages/apps/Launcher3的 QuickStep 模块里,核心类包括RecentsView、TaskView、TaskThumbnailView; - SystemUI 不再直接维护 Recents 页面,而是通过
OverviewProxyService与 Launcher 建立 binder 连接,转发系统事件和手势信号; - system_server 里的
ActivityTaskManagerService负责任务栈和最近任务列表,WindowManagerService负责缩略图快照和动画协调。
一句话总结:Android 11 的 Recents 是“Launcher 出 UI,SystemUI 出输入,system_server 出数据,WindowManager 出动画”的四方协奏。
1.2 四方分工:谁管数据、谁管 UI、谁管动画
我刚入行那会儿看 Recents 源码,最大的障碍是找不到一个叫“Recents”的模块,因为功能被打散到了好几个进程里。这里先把分工理清楚,后面看代码才不会迷路。
| 模块 | 所在进程 | 核心类/组件 | 职责 |
|---|---|---|---|
| ActivityTaskManagerService | system_server | RecentTasksController、TaskPersister | 维护任务栈、最近任务列表、任务启动/移除、持久化 |
| WindowManagerService | system_server | TaskSnapshotController、RecentsAnimationController | 生成和管理任务快照、协调最近任务转场动画 |
| SystemUI | SystemUI 进程 | OverviewProxyService、NavigationBarView | 识别按键/手势输入、转发 Overview 命令、显示状态栏和导航栏 |
| Launcher / QuickStep | Launcher 进程 | RecentsView、TaskView、TaskThumbnailView | 渲染最近任务列表、处理用户点击/滑动/拖拽、发起任务切换请求 |
这张表要能背下来。实际排查问题时,第一步永远是先确定“现象属于哪一层”:列表没数据是数据层的问题,缩略图黑屏大多是快照层的问题,上滑动画卡顿是动画协调层的问题,点卡片没反应是 UI 层和 system_server 的 binder 通信问题。
1.3 QuickStep 的关键地位:没有它,手势 Recents 就不存在
QuickStep 这个名字值得多说两句。严格来说 QuickStep 不是一个应用,而是 Launcher3 的一个编译变体模块,它把 Launcher 的桌面能力和“最近任务 + 手势导航”能力打包在了一起。Android 11 的设备上,系统默认 Launcher 必须是带 QuickStep 编译的,底层手势导航的 Recents 功能才能完整工作。
这也是很多第三方 Launcher 在 Android 11 上不支持手势上滑呼出最近任务的原因——第三方 Launcher 没有实现 QuickStep 提供的IOverviewProxy通信协议。换句话说,Recents 的“开关”实际上是握在 Launcher 手里的,SystemUI 只是中间传话的。理解这一点,你就明白为什么换 Launcher 会导致最近任务样式或行为变化了。
2. 任务数据从哪来:RecentTaskInfo 与任务栈模型
2.1 最近任务列表的本质是一份 Task 清单
先纠正一个常见误解:Recents 列表里的“卡片”不是进程,也不是 App,而是 Task(任务栈)。一个 Task 里可能只有一个 Activity,也可能有多个 Activity 按栈排列。用户看到的一张卡片,本质是RecentTaskInfo这个数据结构的可视化。
在 Android 11 上,Launcher 获取最近任务列表最终走的是ActivityTaskManager.getRecentTasks(),底层由ActivityTaskManagerService.getRecentTasks()实现。关键字段包括:
taskId/persistentTaskId:任务 ID,跨进程标识一个任务;baseIntent/baseActivity:任务栈底部 Activity 的信息,决定卡片点击后拉起哪个界面;topActivity:当前栈顶 Activity,即用户最后看到的界面;description:TaskDescription对象,包含卡片标题、图标、背景色等信息;windowingMode、bounds:窗口模式(全屏/分屏/自由窗口)和窗口边界。
你可以用下面的命令在设备上直接看最近任务数据:
adb shell dumpsys activity recents输出里会列出每个任务的taskId、baseIntent、topActivity、persistentTaskId以及是否hasSnapshot。这个命令是排查 Recents 数据问题的第一利器,我后面还会反复提。
Android 11 的最近任务默认有数量上限,一般由ActivityManagerConstants里的MAX_RECENTS控制,常见设备默认值是 20 条。这个限制是双层的:一是 UI 上最多展示多少张卡片,二是系统内部为“最近任务”这一个用途最多保留多少个 Task。超过上限后,最旧的任务会被移出列表,但注意它并不一定代表任务被销毁,只是不再出现在 Recents 里。
2.2 任务什么时候进入或离开 Recents
一个任务进入 Recents 的时机很直接:当你启动一个应用,系统为它创建 Task 的那一刻,它就成了“最近任务”的一员。但有几个细节容易踩坑:
- 启动 Activity 时如果 Intent 带了
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,这个任务会被标记为“不出现在 Recents”; - 应用调用
finishAndRemoveTask(),任务会从 Recents 列表里被摘除; - 用户在 Recents 里上滑或点删除按钮,系统会调用
IActivityTaskManager.removeTask(),把任务从列表移除。注意,移除任务通常会一并销毁任务栈里的 Activity,但具体是否杀进程取决于系统是否还有该进程的其他组件存活。
之前有同事问:为什么有的应用明明进程还活着,Recents 里却没了记录?大概率就是启动 Intent 带了 exclude-from-recents 标志,或者应用自己在onDestroy里调了finishAndRemoveTask()。
2.3 Launcher 侧的数据模型:RecentsModel 与 TaskKey
Launcher 拿到RecentTaskInfo列表后,不会直接拿原始数据渲染,而是先转成自己的数据模型。Android 11 的 Launcher3 里,每一张后台卡片对应一个TaskKey,TaskKey封装了taskId、displayId、userId这些用于区分任务的身份信息,并且决定了缩略图缓存是否命中。
RecentsModel负责维护任务列表的快照和状态,它在收到系统发来的“最近任务变化”回调后,会重新拉取数据并刷新 UI。系统侧的通知链路是:ActivityTaskManagerService发现任务增删后,通过OverviewProxyService的onRecentTasksChanged()回调通知 Launcher。所以如果你在系统里手动改了任务栈,比如用am命令强制移除任务,Launcher 的 Recents 列表会在回调后自动刷新。
数据加载是异步的。卡片先显示占位背景和标题,缩略图通过ThumbnailCache异步加载,加载完再贴到TaskThumbnailView上。这个设计避免了一次性解码几十张 Bitmap 导致的内存暴涨,但也带来一个副作用:冷启动进 Recents 时,卡片和缩略图是分批出来的,如果快照加载慢,你会看到先卡片后图片的“闪一下”效果。
3. 快照机制:Recents 缩略图不是实时截屏
3.1 TaskSnapshot 是什么、什么时候生成
Recents 卡片中间的大图,很多人以为是实时截屏,其实不是。它是系统维护的一份TaskSnapshot(任务快照),本质是任务某个时刻的一张位图加元数据。这样做的好处很明显:不用在进入 Recents 的瞬间对所有任务做实时截图,直接读缓存图,响应速度快得多。
Android 11 里,快照的生成与保存由两个角色完成:
TaskSnapshotController(system_server):决定什么时候抓取快照,以及从哪个 surface 抓;TaskSnapshotPersister(system_server):负责把快照写到磁盘,并按 LRU 策略清理。
抓取的时机很有讲究,常见触发点包括:任务从前台退到后台、任务被覆盖、启动 Recents 动画时。抓取是异步的,抓到的不是 UI 线程里的 View,而是 WindowManager 里该任务顶层窗口的 surface 内容。为了控制体积,系统默认不会按原始分辨率保存,而是按缩放比例生成缩略图,具体比例不同版本和设备有差异,常见的是原图的一半左右。
快照文件默认存在系统数据目录下。以常见 AOSP 分支为例,一般路径是/data/system_ce/<userId>/snapshots,不同厂商版本可能把目录挪到别处。想看实际路径,可以用 root 后ls目录,或者从dumpsys activity recents的输出里找快照文件名线索。
3.2 为什么你的应用在 Recents 里永远黑屏
这是后台卡片问题里最高频的一个。绝大多数情况是应用给自己设了FLAG_SECURE。一旦窗口设置了FLAG_SECURE,系统既不允许截屏、录屏,也不会生成有效的 TaskSnapshot,WMS 会用一个全黑图替代。典型场景是银行 App、播放 DRM 视频的播放器、某些保密聊天应用。
// 应用侧导致 Recents 黑屏的典型代码 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);如果你确定应用没设FLAG_SECURE,但缩略图还是黑的,排查顺序建议这样走:
dumpsys activity recents看任务的hasSnapshot是否为 true;- 检查快照文件是否存在,文件是否存在且大小正常;
- 确认任务是否刚被冷启动过、还没触发过快照抓取;
- 确认是不是在开发者选项里关了“不保留活动”或开了某些省电策略,导致任务列表数据异常。
还有一个容易忽略的场景:应用处于“安全窗口”重叠状态时去抓快照,比如系统弹了隐私权限对话框,抓到的图可能不是你当前界面。这个属于时序问题,系统会通过快照更新的回调机制尽量规避,但厂商定制时如果改了窗口层级,就很容易踩中。
3.3 快照不更新的问题:为什么都回桌面了,缩略图还是旧的
快照生成是“事件驱动”的,不是“时间驱动”的。一个应用切到后台,系统抓一张图存好;如果之后应用在后台偷偷更新了 UI,但前台没有触发新的抓取,那 Recents 里的缩略图就还是旧画面。这在 AOSP 里是正常行为,不是 bug。
做系统定制的时候,如果希望缩略图更“新鲜”,一般有几个思路:
- 按需刷新:在任务可见性变化时增加抓拍时机,让系统在
onStop后马上抓一张; - 主动失效:在应用的
onPause或onStop里通过系统接口让旧快照失效,下次进 Overview 时重新抓取; - 定制缩略图比例:对超宽屏或折叠屏,调整快照缩放参数,避免图片被拉伸变形。
我的建议是:如果业务对缩略图实时性要求很高,优先考虑用“失效旧快照 + 按需重新生成”的方案,而不是频繁抓拍,频繁抓拍会明显增加内存和磁盘 IO 开销。
4. 手势触发与动画链路:从“上滑”到“卡片出现”的完整路径
4.1 输入到行为:三键导航和手势导航两条链路
Android 11 上进入 Recents 有两条入口路径,但最终都会汇合到同一个系统调用点。
三键导航的路径比较传统:用户点“方块键”,SystemUI 的NavigationBarView收到按键事件,调用toggleOverview()之类的接口,请求进入最近任务。这条链路相对简单,适合做基础定制。
手势导航的路径是 Android 10 之后的重头戏:用户从底部上滑,SystemUI 的GestureNavigationHelper内部会先判断是“上滑进入 Recents”还是“上滑回桌面”。判断的依据包括滑动手势的起始位置、速度、当前是否在桌面等。如果判定进入 Recents,SystemUI 会通过OverviewProxyService通知 Launcher 显示 Overview,同时调用 system_server 的窗口接口启动 Recents 转场动画。
我这里给一张核心交互的伪代码流程,方便理解:
// 伪代码:SystemUI 侧触发 Recents // 1. 识别手势 if (gestureHelper.isSwipeUpFromBottom()) { // 2. 告诉 Launcher 准备显示 Overview overviewProxy.startOverview(); // 3. 请求 WindowManager 启动动画 windowManager.startRecentsAnimation(...); }很多人不知道的是:上滑进入 Recents 时,当前应用的窗口并不会立刻销毁,而是作为动画纹理继续存在,由 Launcher 的 Overview 界面“接管”。这保证了用户看到的是同一个窗口从全屏缩小到卡片里的连贯效果,而不是生硬的画面切换。
4.2 RecentsAnimationController:动画的幕后调度者
RecentsAnimationController是 system_server 里的动画协调核心。它的职责是同时管理多个进程的窗口状态,确保 transitions 一致。它在动画期间会:
- 冻结或延迟当前应用窗口的部分回调(比如避免应用在动画过程中频繁重绘);
- 把当前应用的窗口“借给”Launcher 作为动画目标;
- 监听用户手指抬起/松手,决定是进入 Overview 还是返回应用;
- 在动画结束后,统一回调所有参与者的完成/取消状态。
这个类也是很多“动画卡顿”问题的源头。假如某个应用在onPause里做了重量级操作,或者应用窗口的 surface 被频繁更新,动画期间 Launcher 会明显掉帧。我查过不少此类问题,最后结论都是应用侧在生命周期回调里做了太多事。
对应用开发者来说,如果想保证进 Recents 动画流畅,有两个非常实际的经验:
- 不要在
onPause里做耗时操作,比如写数据库、同步网络、大量 Bitmap 解码; - 不要在窗口注册复杂的
OnPreDrawListener,它会影响 surface 绘制频率和动画合成效率。
4.3 卡片点击、滑动删除、分屏的底层动作
用户对卡片的操作,最终都会转化为 system_server 的任务管理调用。
- 点击卡片:Launcher 发起
startActivityFromRecents(taskId, options),system_server 找到对应任务,把它moveToFront并恢复窗口。App 侧感知到的就是 onRestart / onNewIntent 等正常生命周期。 - 滑动删除:Launcher 调用
IActivityTaskManager.removeTask(taskId, ...),任务从 Recents 移除,Task 里的 Activity 被销毁。注意这里只删任务,不保证杀进程。 - 分屏:Android 11 里从 Overview 进入分屏的入口是长按或拖动任务卡片(不同设备上入口略有差异)。本质上系统会为两个任务设置分屏窗口模式(
WINDOWING_MODE_SPLIT_SCREEN_PRIMARY/SECONDARY),并按屏幕区域分配 bounds。这一步会同时影响两个任务的快照尺寸和位置。
如果你只做 Launcher 定制,Android 11 的这些交互在TaskView的点击/拖拽回调里都有明确入口,改起来不难。真正麻烦的是跨进程的状态同步,比如分屏时两个任务的缩略图面积不同、快照尺寸需要重新生成,这部分逻辑分散在 system_server 和 Launcher 两侧,改之前一定要先画清楚状态机。
5. 常见问题与调试实录
5.1 Recents 列表空白 / 卡片数量不对
现象是打开后台页,一张卡片都没有,或者明显比实际打开的应用少。
排查思路:先确认系统侧有没有任务数据。执行:
adb shell dumpsys activity recents如果输出里mTasks显示为空,说明 system_server 里就没存任务。原因可能是:任务都被移除了、系统设置的最大任务数被改成了 0、或者当前用户是刚解锁的次级用户,任务数据还没加载完。
如果系统侧有数据但 UI 空白,问题大概率在 Launcher 侧的拉取流程。看日志里的QuickStep或OverviewProxy关键字:
adb logcat | grep -E "QuickStep|OverviewProxy|Launcher"常见原因是 Launcher 和 SystemUI 之间的 binder 连接断了,OverviewProxyService没有成功绑定,Launcher 收不到onRecentTasksChanged回调。一般重启 SystemUI 或 Launcher 就能恢复,但长期反复断连就要检查是不是改了IOverviewProxy接口导致版本不匹配。
5.2 缩略图黑屏 / 显示旧图 / 白屏
黑屏的原因在上文已详细讲过,优先查FLAG_SECURE。白屏则是快照没抓到但系统还是用空白图占位了,常见于内存吃紧时TaskSnapshotController抓拍失败、或者磁盘写入慢还没落盘。
旧图问题一般不是 bug,而是快照更新时机决定的。如果你在做定制,希望每次进 Recents 都拿到最新画面,可以走“删除旧快照 + 请求重新抓拍”的方案。在调试阶段,你可以手动清理快照目录来验证加载链路:
adb root adb shell rm -rf /data/system_ce/0/snapshots adb shell stop && adb shell start注意:这个操作会清掉当前用户所有任务的快照,重启后系统会按需重新生成。只适合开发调试,别在用户设备上乱用。
5.3 上滑动画卡顿 / 手势识别不灵敏
动画卡顿优先看 Launcher 侧的渲染负载。用dumpsys gfxinfo抓 Launcher 的帧数据:
adb shell dumpsys gfxinfo com.android.launcher3重点看Janky frames和Frame miss指标。如果卡顿只发生在某个应用切换到 Recents 的瞬间,看这个应用是否有高频刷新或 WebView 类界面。WebView 在动画期间依然在渲染网页内容,非常容易导致 surface 合成压力大。
手势识别不灵敏则多半是应用自己的锅。Android 11 支持应用通过setSystemGestureExclusionRects声明底部区域的系统手势排除区,很多游戏和自定义播放器会把底部边缘大片区域设成排除区,导致系统上滑手势需要“更用力”才能触发。这个不是 Recents 代码的问题,但排查时很容易误判成 Recents 失效。
5.4 调试命令速查
| 命令 | 用途 |
|---|---|
adb shell dumpsys activity recents | 查看最近任务列表、每个任务的快照状态 |
adb shell dumpsys activity activities | 查看任务栈内 Activity 的组织关系 |
adb shell dumpsys window windows | 查看窗口层级和可见窗口,辅助分析缩略图来源 |
adb shell dumpsys SurfaceFlinger --list | 查看 Surface 层,确认快照抓取使用的 surface 是否存在 |
adb logcat -s QuickStep OverviewProxy Launcher | 查看 Launcher 与系统之间的通信日志 |
adb shell dumpsys gfxinfo <Launcher包名> | 查看 Launcher 渲染帧率与 jank 统计 |
这组命令基本可以覆盖 80% 的 Recents 排查场景。剩下的 20%,几乎都在“快照”和“动画时序”这两个深水区里,需要结合源码逐步加日志定位。
6. 周边联动:多任务族、投屏与外设场景
6.1 分屏、气泡、画中画和 Recents 的关系
Android 11 的多任务体验不只有 Recents,还有分屏、气泡通知、画中画(PiP),它们之间是有联动关系的。
分屏时,两个任务各自有 bounds 和窗口模式,Recents 里对应的卡片会保留各自的快照。如果分屏的某一侧任务被关闭,另一侧窗口会自动扩展为全屏,这时 Recents 里那条任务的快照和窗口状态也要同步更新。定制分屏功能时,最容易漏的就是忘记同步刷新 Recents 的数据模型。
气泡(Bubble)从 Android 11 起正式开放,它本质是悬浮在应用之上的一个小窗口。气泡展开时,底层任务不一定变化,Recents 里的卡片一般仍显示展开前的界面。画中画则是一个特殊的多窗口形态,任务还在后台,窗口以 PiP 形式浮在最上层,Recents 缩略图抓取时如果 PiP 窗口和任务窗口分属不同层级,可能会出现缩略图内容被 PiP 窗口遮挡或没有 PiP 内容的问题。这类问题没有统一解法,得看具体窗口层级配置。
6.2 投屏 / 多显示设备上的最近任务处理
Android 11 对多显示的支持已经比较成熟了。外接显示器、投屏接收端、车机屏幕,都可能让应用跑到非默认显示设备上。Recents 的数据源getRecentTasks()是支持按 display 过滤的,也就是说,主屏 Launcher 的最近任务列表和副屏上的任务可以互不干扰。
实际开发中常见两个坑:
第一个坑是缩略图抓取的目标屏幕不对。一个应用显示在副屏,主屏抓快照时指定了错误的 displayId,抓回来的图是黑的或空的。排查方式是看dumpsys activity recents输出里的displayId字段,确认任务挂在哪块屏。
第二个坑是远程投屏场景下的内容保护。很多视频类应用知道自己会显示在投屏设备上,会主动设置FLAG_SECURE或使用 DRM 保护,这种情况下无论投屏画面还是 Recents 缩略图都会黑屏。这是产品设计层面的取舍,不是 bug。
如果你在做支持桌面模式或外部显示的 ROM,建议系统定制时在TaskSnapshotController里增加 display 维度的开关,至少保证主屏任务的快照不受副屏内容干扰。
6.3 USB OTG 外设对任务管理的间接影响
把 USB OTG 和 Recents 放在一起看,听着有点跳跃,但实际调试中确实会遇到。
OTG 外接键盘鼠标后,部分定制 ROM 会在桌面或 Launcher 里支持快捷键切换任务,比如 Alt+Tab 或自定义组合键。这类快捷键最终也是调用 system_server 的任务切换接口,本质上和 Recents 是同一条路。如果快捷键切换无效,先去查 SystemUI 或 Launcher 是否实现了对应的按键监听,别直接在 Recents 代码里找问题。
OTG 外接 HDMI 扩展显示或扩展坞时,新窗口可能创建在外部显示设备上,任务管理器的 displayId 会跟着变化。这种多窗口任务在 Recents 里的可见性取决于getRecentTasks的参数过滤,部分 ROM 默认不展示非主屏任务,导致用户以为任务丢了。要解决,可以在 Recents 数据层去掉 displayId 过滤,或为副屏任务单独做一个“外部任务面板”。
至于 OTG 外接存储,它和 Recents 没有直接关系,但会影响应用的 Activity 重建:应用读写外部存储过程中遇到挂载变化,系统可能重建 Activity,导致任务栈顶变化,进而影响topActivity字段。如果你发现某个任务在 Recents 里的标题或图标偶尔错乱,可以往这个方向查。
6.4 我的经验:把“任务生命周期”当作排查主线
和 Recents 打了这么多年交道,我的体感是:Recents 本身逻辑不复杂,复杂的是它串联起来的任务生命周期和窗口转场。几乎每一个看似玄学的问题,最后都能归到“某个 Task 在某个时间点处于哪个状态、有没有对应的窗口和快照”这个问题上。
所以我遇到 Recents 相关 bug,从来不会一上来翻 UI 代码,而是先做两件事:
dumpsys activity recents看数据层是否正常;- 复现问题后立刻
dumpsys window windows对比窗口层状态。
这两步能筛掉一半以上的表面问题。剩下的动画时序问题,再在RecentsAnimationController和TaskSnapshotController里加日志,观察关键回调的先后顺序,基本都能定位。
Android 11 这套 Recents 架构,本质上是在为后来的版本铺路。到 Android 12 之后,预测性返回、桌面任务栏、折叠屏适配都建立在这套“Launcher 持有 Overview + system_server 管任务窗口”的骨架上。把 Android 11 这条主线吃透,后面再看新版本会轻松很多。这篇文章里的命令和排查思路,都是可以直接拿去做实验的;建议你手头准备一台 Android 11 的调试机,按本文顺序把每个环节 dump 一遍,比光看文章记代码要扎实得多。