37、开机启动优化:bootchart、init进程、zygote启动、SystemServer启动

开机慢,是Android开发中绕不开的痛。

我记得有一次,客户反馈某款设备开机要40秒,PM直接拍桌子说“砍到20秒以内”。那段时间我天天跟bootchart打交道,盯着init进程、zygote、SystemServer的启动日志,一条一条地抠时间。说实话,开机优化这事儿,没有捷径,就是靠工具和经验的结合。

37.1 先搞清楚开机流程

Android开机,说白了就是一条链:

  • Bootloader加载内核
  • 内核启动init进程(PID=1)
  • init解析.rc文件,启动关键服务
  • zygote启动,预加载Java框架
  • SystemServer启动,拉起各种系统服务
  • Launcher出现,用户看到桌面

嗯,这里要注意:每一步都可能成为瓶颈。我见过最夸张的情况,是某个厂商在init.rc里写了200多个服务,光串行启动就耗了15秒。

核心观点:开机优化的本质,就是“减少串行,增加并行,砍掉冗余”。

37.2 bootchart:你的第一把刀

bootchart是Google官方提供的开机性能分析工具。说白了,它就是把开机过程中每个进程的CPU、磁盘I/O、等待时间画成一张图。

怎么用?很简单:

# 1. 开启bootchart
adb shell 'echo 120 > /data/bootchart/start'  # 采集120秒
# 2. 重启设备
adb reboot
# 3. 拉取数据
adb pull /data/bootchart/ .

然后你会得到一个.tgz文件,解压后用bootchart工具生成SVG图。我个人习惯用Python版的pybootchartgui,更直观。

看bootchart图,我一般关注三个点:

  • CPU利用率曲线——如果长时间低于50%,说明有等待锁或I/O瓶颈
  • 进程启动时间线——看哪些服务是串行的,能不能改成并行
  • 磁盘I/O峰值——如果I/O长时间100%,说明存储是短板

避坑指南:我曾经遇到一个案例,bootchart显示zygote启动花了8秒,但实际代码里没什么耗时操作。后来发现是eMMC的随机读性能太差,换了个UFS就降到3秒了。所以,别光看软件,硬件也可能是瓶颈

37.3 init进程:开机第一道关卡

init是Linux内核启动后的第一个用户空间进程。它负责解析init.rc,然后按顺序启动各种服务。

这里有个关键点:init.rc中的服务默认是串行启动的。你想想看,如果A服务依赖B服务,那没问题。但如果A和B完全独立,还串行启动,那就是浪费时间。

优化思路:

  • 使用class关键字分组——把不依赖的服务放到同一个class里,并行启动
  • 减少不必要的服务——有些厂商喜欢在init.rc里加一堆调试服务,开机后根本用不到
  • 延迟启动非关键服务——比如logd、adbd,完全可以等桌面出现后再启动
# 示例:将核心服务分组
on boot
    class_start core          # 核心服务,必须早启动
    class_start main          # 主要服务,可以稍晚

# 非关键服务延迟启动
service logd /system/bin/logd
    class late_start          # 延迟启动
    user root
    group root

嗯,这里要注意:别乱改init.rc。我曾经见过有人把surfaceflinger放到late_start,结果开机直接黑屏。每个服务的依赖关系,一定要搞清楚。

37.4 zygote启动:Java世界的入口

zygote是Android Java框架的起点。它启动后,会预加载常用的Java类和资源,然后监听socket,等待创建新进程。

zygote启动慢,通常有两个原因:

  • 预加载的类太多——默认有2000多个类,每个都要加载、验证、初始化
  • 资源预加载——比如主题资源、系统字体,都是I/O密集型操作

优化手段:

  1. 裁剪预加载类——用preloaded-classes文件,去掉不常用的类。我一般先跑一遍开机流程,用trace分析哪些类真的被用到了
  2. 资源懒加载——有些资源可以等用到时再加载,不必全塞在zygote里
  3. 使用compiler filter——如果是Android 10以上,可以调整PGO(Profile Guided Optimization)策略,让常用代码提前编译

警告:裁剪预加载类要谨慎。我有个同事把android.app.Activity从预加载列表里删了,结果所有应用启动都变慢了,因为每次都要重新加载。所以,只删那些确定不用的类

37.5 SystemServer启动:服务大杂烩

SystemServer是zygote fork出来的第一个Java进程。它负责启动所有系统服务,比如ActivityManagerService、WindowManagerService、PackageManagerService等等。

这里有个经典问题:服务启动顺序。有些服务之间有依赖关系,比如WMS依赖AMS,AMS又依赖PMS。如果顺序搞错了,就会死锁或者空指针。

我一般用systrace来抓SystemServer的启动过程:

# 抓取开机阶段的systrace
adb shell atrace --async_start -b 40960 am wm pm
# 等待开机完成
adb shell atrace --async_dump > trace.txt

看systrace时,重点关注:

  • 每个服务的onStart耗时——超过100ms的就要怀疑
  • 服务之间的等待时间——比如PMS在扫描包时,AMS在等它,这段时间就是浪费的
  • Binder调用次数——过多的跨进程调用会拖慢启动

优化思路:

  • 并行启动独立服务——比如NetworkManagementService和BatteryService,没有依赖关系,可以同时启动
  • 延迟启动非关键服务——比如WallpaperManagerService,用户不会一开机就换壁纸
  • 减少包扫描时间——PMS扫描包是SystemServer启动的瓶颈之一。可以优化包缓存,或者只扫描系统应用,用户应用等开机后再扫描

个人经验:有一次,我发现SystemServer启动花了12秒,其中PMS占了7秒。后来发现是某个预装应用有1000多个so文件,PMS在扫描时花了大量时间。跟产品沟通后,砍掉了那个应用,开机时间直接降到8秒。所以,有时候优化不是技术问题,是产品决策问题

37.6 一张图看懂开机优化

下面这张图,是我自己总结的开机优化知识体系。你可以把它当作一个检查清单:

Android开机优化知识体系 bootchart分析 CPU/I/O/进程时间线 init进程优化 并行启动/延迟启动 zygote启动优化 预加载裁剪/懒加载 SystemServer优化 服务并行/包扫描优化 关键指标:开机总时间 ≤ 20秒(行业标准) | 桌面出现时间 ≤ 15秒 | 用户可操作时间 ≤ 18秒 bootchart分析要点 • CPU利用率曲线 • 进程启动时间线 • 磁盘I/O峰值 • 等待锁分析 • 关键路径识别 init优化手段 • class分组并行 • 延迟启动非关键 • 减少冗余服务 • 优化.rc解析 • 使用on boot优化 zygote优化手段 • 裁剪预加载类 • 资源懒加载 • 调整compiler filter • 优化预加载顺序 • 减少JNI调用 SystemServer优化 • 服务并行启动 • 延迟非关键服务 • 包扫描优化 • 减少Binder调用 • 使用systrace分析 总结:先分析(bootchart)→ 再优化(init/zygote/SystemServer)→ 最后验证(systrace + 秒表)

37.7 实战:一个完整的优化案例

最后,分享一个我实际做过的优化案例。某款平板设备,开机时间35秒,目标20秒。

第一步,用bootchart分析:

  • init阶段:8秒(串行启动了30多个服务)
  • zygote阶段:6秒(预加载了2800个类)
  • SystemServer阶段:12秒(PMS扫描包花了6秒)
  • 其他:9秒(包括SurfaceFlinger、显示初始化等)

第二步,针对性优化:

  1. init阶段:把独立服务改成并行,从8秒降到4秒
  2. zygote阶段:裁剪掉400个不常用的类,从6秒降到4秒
  3. SystemServer阶段:优化PMS包缓存,从12秒降到7秒
  4. 其他:延迟启动壁纸服务、输入法服务,省了2秒

最终结果:35秒 → 17秒。嗯,客户很满意。

核心总结:开机优化没有银弹。bootchart帮你找到瓶颈,init/zygote/SystemServer是三个主要战场。每个战场都有对应的优化手段,关键是先测量,再动手。别一上来就改代码,很可能改了半天,发现瓶颈在别的地方。


公众号:蓝海资料掘金营,微信deep3321