1. WMS 启动流程概述
做启动耗时优化,你得先把 WMS 的启动过程摸透。我之前带团队排查一个冷启动闪白屏的问题,折腾了两天,最后发现是 WMS 里某个阶段等了一把锁,白白多花了 800 毫秒。从那时起,我就养成了一个习惯——先画流程,再动手。
这一章我们先把基础打牢。WMS 启动到底分几个阶段?涉及哪些关键组件?耗时怎么定义、怎么算?这些问题搞清楚了,后面的监控才有方向。
1.1 启动阶段划分
说白了,WMS 不是一下子起来的,它是一层层推上去的。我个人习惯把它分成 3 个大阶段、6 个小阶段:
- 预初始化阶段:内核通过 init 进程拉起 SurfaceFlinger、创建 Display 设备。这时 WMS 还没出生。
- 核心初始化阶段:SystemServer 启动后,在 ServerThread 中创建 WindowManagerService 实例、初始化 WindowPolicy、DisplayManager、InputManager 等关键组件。
- 同步与就绪阶段:WMS 等待 SurfaceFlinger 的帧反馈、等待各 Display 准备完成,最后发出 `BOOT_COMPLETED` 广播。
嗯,这里要注意:你真正关心的“WMS 启动耗时”,一般不包含预初始化阶段。因为它跟 WMS 自身代码关系不大,更多是底层驱动的锅。
WindowManagerService.main() 开始,到 systemReady() 回调完成为止。
1.2 关键组件一览
WMS 启动过程中,至少有这几个“老人”需要你记住。我列个表格,方便你在分析 trace 时对照:
| 组件名称 | 职责简介 | 启动中耗时敏感点 |
|---|---|---|
| WindowManagerService | 核心,管理窗口层级、尺寸、动画 | 构造耗时、初始化策略、注册监听 |
| DisplayManagerService | 管理物理/虚拟屏幕 | 热插拔检测、Display 对象创建 |
| InputManagerService | 输入事件分发 | 读取配置、监听通道建立 |
| WindowPolicy | 系统窗口策略(状态栏、导航栏等) | 读取配置、创建系统窗口布局 |
| SurfaceFlinger | 图形合成,WMS 的“下游”依赖 | 帧反馈等待、VSync 初始化 |
你有没有发现,真正的耗时大头往往不在 WMS 本身,而在它依赖的那些服务。我记得在一个项目中,InputManagerService 读取一根奇怪的 USB 键盘配置,硬生生多了 300ms——这就是典型的外设兼容性陷阱。
1.3 启动耗时的定义与计算方式
先统一口径:我们说的“WMS 启动耗时”,是指从 WMS 开始初始化 到 WMS 报告自己就绪 的时间差。怎么算?
方案一(最常用):抓 Slog 中的时间戳。
// 启动开始标记(在 WindowManagerService.main 中添加)
Log.d("WMS_Start", "init begin: " + SystemClock.elapsedRealtime());
// 启动结束标记(在 systemReady 回调中添加)
Log.d("WMS_Start", "init end: " + SystemClock.elapsedRealtime());
然后你用 `adb logcat -s WMS_Start` 拉出来,两个时间戳相减就行。曾经我就靠这个简单的打点,定位到某个定制 ROM 里 WMS 启动慢了 1.2 秒——后来发现是策略里加了一个大文件解码。
方案二(更精细):用 Perfetto / Systrace 圈出区间。
你可以在系统 trace 配置里开启 wm.trace_tag,大概长这样:
Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "WMS.init");
// ... 初始化逻辑 ...
Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
然后打开生成的 trace 文件,直接看区间时长。这个方法比打 log 更准,因为 trace 能精确到微秒,而且能叠加 CPU 调度信息。
另外,计算方式还有一点要注意:要不要排除休眠等待的时间?
我个人习惯算实际经过时间(wall time),因为用户感受到的就是从按下电源到看到桌面的总时长。但如果你在做性能回归,最好同时记录 CPU 活跃时长,两者对比能帮你判断是 CPU 资源不足还是代码逻辑慢。
1.4 知识结构总览
为了让你对整个启动流程有直观的认识,我画了一张图。它把阶段、组件、以及它们之间的依赖关系串在了一起。后续每一章我们都会围绕这张图展开。
图里的三个阶段是串行的,但每个阶段内部的子步骤可能并行。比如在核心初始化阶段,DisplayManager 和 InputManager 是先后创建的,但它们的初始化可以跑在独立线程上(嗯,这块后面章节会细讲)。