3. SystemServer 初始化中的性能监控需求

说到 SystemServer 的初始化,很多人第一反应是“把服务都 start 起来就行”。但我在项目中踩过太多次坑了——服务启动顺序、锁竞争、IPC 超时,任何一个环节出问题,手机开机就要多等好几秒。所以这一章,咱们专门聊聊性能监控到底要盯哪些东西。

3.1 为什么要做性能监控?

说白了,系统启动是用户第一眼看到的体验。如果冷启动花了 30 秒,用户早就把手机摔了。我见过一个案例:某款设备升级后,开机动画播了 15 秒还没进入桌面,最后定位到是 PMS 扫描包时频繁触发 GC。

性能监控的好处很明显:

  • 快速定位瓶颈 – 哪个服务耗时异常,一眼就能看到
  • 量化优化效果 – 改之前 5.2s,改之后 4.7s,有数据才靠谱
  • 防止回归 – 版本迭代时自动告警,避免新改动拖慢启动
我个人习惯在每一版固件发布前,跑一遍全链路监控,记录所有关键节点的时间戳。哪怕这次没改 SystemServer,也能知道有没有意外退化。

3.2 核心监控维度

不要什么都埋点,那样只会制造垃圾数据。我把常用的维度整理成一个表格,你照着清单做就行。

维度指标说明
时间阶段耗时、总耗时每个 Service.onStart 到 handleMessage 的时间
CPU占用率、掉帧数启动阶段 CPU 是否飙到 100%?有没有明显掉帧
内存PSS、GC 次数SystemServer 进程内存增长曲线,GC 是否频繁
锁竞争锁等待时间、死锁主线程 wait 太久会导致 ANR
IPCBinder 调用耗时跨进程调用是否超时,尤其 service 之间的依赖
I/O读写耗时、路径解析 manifest、扫描 data 目录很耗 I/O
这里我建议优先关注「时间」和「CPU」,因为它们是用户体验最直接的映射。等优化到一定程度,再深入看锁和 IPC。

3.3 关键埋点阶段与分类

SystemServer 初始化不是一根直线,而是分阶段并行处理。我们需要把埋点分散到每个阶段入口和出口。下图是我常用的阶段划分:

SystemServer 初始化性能监控关键阶段 Zygote 预加载 类加载 / 资源预置 主线程启动 SystemServer.main() ServiceManager 注册核心服务 关键系统服务 AMS / PMS / WMS 辅助服务 Alarm / Telephony Framework 服务 PackageManager / ContentService 系统就绪广播 BOOT_COMPLETED / user unlock 性能监控关注点: 每个阶段入口/出口埋点 → 记录耗时、CPU、内存;单独对 Binder 调用计数和超时监控 锁竞争通过主线程 MessageQueue 的 IdleHandler 采集;GC 次数由 Debug.startMethodTracing 配合

从图上你能看到,我习惯把埋点分为三个阶段:预加载期(Zygote)、服务启动期(SystemServer 主线程 + 各服务)、广播通知期。每个阶段的监控指标侧重点不同。比如预加载期重点关注类加载数量和耗时,服务启动期要看锁竞争和 IPC 延迟,广播期则要看接收者处理时长。

3.4 常见埋点误区与避坑

做埋点容易犯几个错误,我列出来希望你能避开:

  • 埋点本身影响性能 – 比如每个函数都加 log,启动时间直接翻倍。建议用异步写日志,或者只在 debug 版本开启。
  • 时间戳精度不够 – 用 SystemClock.elapsedRealtime() 而不是 currentTimeMillis(),后者会被 NTP 修改。
  • 忽略并发场景 – SystemServer 里多个线程同时跑,如果只统计主线程,可能漏掉 work thread 的瓶颈。
  • 依赖顺序假设 – 我曾经以为 Service A 一定在 Service B 之前启动,结果某次改动了启动顺序,埋点数据全乱了。
特别注意:不要在 Service.onStart() 里做耗时操作,否则直接卡住主线程。如果真的要统计,用 Handler.post 抛到空闲时再记录。

另外,我建议你在正式埋点前先跑一个空监控版本(只加时间戳,不做任何额外操作),测量 overhead。如果埋点使启动耗时增加超过 1%,就需要优化埋点方式。

3.5 小结