15、启动优化:Bootchart 分析、init.rc 优化、Zygote 预加载、SELinux 策略裁剪
系统启动慢,是嵌入式Android开发里最常被吐槽的问题。用户按下电源键,盯着logo看十几秒甚至半分钟,体验能好才怪。我这些年调过的板子,从车机到智能音箱,启动时间从45秒压到12秒的案例都有。说白了,启动优化就是跟时间赛跑,每一毫秒都得抠。
这一章,我们聚焦四个核心手段:Bootchart 分析、init.rc 优化、Zygote 预加载、SELinux 策略裁剪。它们分别对应“测量瓶颈”、“精简流程”、“加速Java世界”、“减少安全校验开销”。嗯,咱们一个一个来。
15.1 先别动手,用 Bootchart 看清现状
没有数据就做优化,那是瞎搞。Bootchart 是 Android 内置的启动性能分析工具,能画出每个进程的 CPU、IO 时间线。我个人习惯,拿到一个新板子,第一件事就是开 Bootchart 跑一次冷启动。
BoardConfig.mk 中设置 BOARD_BOOTCHART := true,或者直接 adb 打补丁。重启后,数据会生成在 /data/bootchart/ 下。
拿到 bootchart.tgz 后,用 pybootchartgui 渲染成 SVG。你会看到类似这样的输出:
# 生成图表
pybootchartgui -o bootchart.svg bootchart.tgz
图表里,横轴是时间,纵轴是进程。重点关注几个东西:
- init 进程的 CPU 占用 —— 如果它长时间跑满,说明 .rc 文件里服务太多或串行化严重。
- Zygote 启动时间 —— 通常 1~2 秒,如果超过 3 秒,预加载列表可能有问题。
- SurfaceFlinger 和 SystemServer 的等待 —— 经常因为 SELinux 权限检查卡住。
我记得有一次,Bootchart 显示 init 在 on boot 阶段卡了 4 秒。查了半天,发现是某个硬件服务的 class_start 依赖了一个还没上电的外设。嗯,这种坑,没有图表根本想不到。
15.2 init.rc 优化:能并行就别串行
init.rc 是 Android 启动的“总指挥”。默认的 init.rc 为了兼容性,写了很多通用服务。但嵌入式设备硬件固定,很多服务根本用不上。我建议你直接创建一个 init.${ro.hardware}.rc,只保留必要的。
优化的核心思路就两条:删掉无用的,并行启动有用的。
15.2.1 删掉冗余服务
举个例子,车机上根本不需要 adbd 默认启动,也不需要 drm 服务(除非有 DRM 内容)。在 .rc 文件里直接注释掉:
# 注释掉不需要的服务
#service adbd /system/bin/adbd
# class core
# disabled
还有 logd、storaged 这些,如果产品不需要详细日志,也可以延迟启动或禁用。我做过一个项目,光删掉 5 个无用服务,启动时间就快了 1.2 秒。
15.2.2 使用并行启动
默认很多服务是 class core 或 class main,它们会按顺序启动。你可以把不依赖先后关系的服务改成 class late_start,或者用 trigger 手动控制。
# 在 init.rc 中定义并行触发
on boot
# 启动核心服务
class_start core
# 立即触发非关键服务
trigger non_critical_start
on non_critical_start
class_start late_start
这样,核心服务启动的同时,非关键服务(比如蓝牙、WiFi 扫描)可以并行跑。你想想看,如果所有服务都串行,CPU 在等 IO 的时候只能干瞪眼,多浪费。
surfaceflinger 和 bootanimation 并行启动,结果动画花屏了。后来加了 wait_for_property 才解决。
15.3 Zygote 预加载:把时间花在刀刃上
Zygote 是 Android Java 世界的“孵化器”。它启动时会预加载大量类和资源,这样后续 App 启动就快了。但默认的预加载列表是给手机设计的,里面有很多嵌入式设备用不到的东西。
预加载的代码在 frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 的 preload() 方法里。它会依次加载:
- 预加载类 —— 从
preloaded-classes文件中读取 - 预加载资源 —— 比如
android.R里的常用 drawable、layout - 预加载 OpenGL —— 如果设备没有 GPU,这步纯属浪费
我建议你这样做:
- 先跑一次启动,用
traceview或systrace抓 Zygote 阶段的耗时。 - 打开
frameworks/base/preloaded-classes,把那些你设备上永远不会用到的类注释掉。比如android.webkit.WebView、android.telephony.TelephonyManager(纯 WiFi 设备)。 - 资源方面,如果设备是横屏的,可以删掉竖屏相关的 layout 资源预加载。
# 在 preloaded-classes 中注释掉不需要的类
# android.webkit.WebView
# android.telephony.TelephonyManager
# android.hardware.camera2.CameraManager
我记得有一次,一个智能音箱项目,Zygote 预加载了 800 多个类,其中 300 多个根本用不上。裁剪之后,Zygote 启动时间从 2.8 秒降到了 1.5 秒。效果立竿见影。
15.4 SELinux 策略裁剪:安全与速度的平衡
SELinux 是 Android 的安全基石,但也是启动性能的“隐形杀手”。每次文件访问、进程通信,都要经过策略检查。如果策略文件太大,或者有大量冗余规则,启动时内核光检查权限就忙不过来。
嵌入式设备通常功能固定,不需要像手机那样支持几百种硬件。所以,裁剪 SELinux 策略 是启动优化的关键一步。
15.4.1 从哪里入手?
策略文件在 system/sepolicy/ 下。我一般先看 domain.te 和 file_contexts。把那些跟设备无关的 domain 删掉,比如 hal_graphics_composer(如果没有独立 GPU)、hal_light(如果没有呼吸灯)。
# 在 domain.te 中注释掉不需要的 domain
# neverallow { domain } self:capability sys_admin;
# 如果设备不需要 sys_admin 能力,直接删掉这条规则
另外,file_contexts 里有很多路径是手机特有的,比如 /data/media、/storage。嵌入式设备如果只有单一用户,可以大幅简化。
15.4.2 使用 permissive 模式调试
裁剪时很容易误伤。我建议先全局设为 permissive 模式,启动后用 avc.log 看哪些规则被触发了。然后只保留这些规则。
# 在 BoardConfig.mk 中设置
BOARD_USE_SELINUX_PERMISSIVE := true
等系统稳定了,再切回 enforcing 模式。我曾经因为删了一条 allow system_server self:capability net_admin,导致网络功能全挂。还好有 permissive 模式,不然排查起来真要命。
audit2allow 工具,把 avc 日志直接转换成需要的 allow 规则。这样既安全又高效。
15.5 综合优化案例:一个车机项目的实战
最后分享一个我实际做过的案例。某款车机,冷启动时间 38 秒,客户要求压到 18 秒以内。我们做了以下操作:
| 优化项 | 操作 | 节省时间 |
|---|---|---|
| Bootchart 分析 | 发现 init 在等待 GPS 服务超时 | 定位问题 |
| init.rc 优化 | 删除 3 个无用服务,并行启动蓝牙和 WiFi | 2.3 秒 |
| Zygote 预加载 | 裁剪掉 200 个类,删除 OpenGL 预加载 | 1.8 秒 |
| SELinux 策略 | 删除 40% 的冗余规则,合并 domain | 1.5 秒 |
| 其他(内核、驱动) | 关闭串口调试、优化存储读写 | 3.0 秒 |
最终冷启动时间降到了 16.2 秒。虽然没到 15 秒以内,但客户已经满意了。嗯,优化就是这样,越往后越难,每一秒都是硬骨头。
启动优化没有银弹。Bootchart 帮你找到问题,init.rc 和 Zygote 帮你减少工作量,SELinux 裁剪帮你减少安全开销。把这四招练熟,大部分嵌入式设备的启动时间都能压到合理范围。