一、从30秒到15秒:一场真实的启动优化战役
说实话,接到这个项目时,我心里是有点打鼓的。某品牌旗舰机,用户反馈开机太慢,竞品都在15秒以内,我们还在30秒徘徊。老板拍桌子说:「一个月,给我压到15秒。」
嗯,今天我就把这场战役的完整过程拆给你看。包括那些踩过的坑、绕过的弯,还有SELinux策略调整的实战经验。
核心目标:将系统启动时间从30秒压缩至15秒以内,同时保证SELinux安全策略不降级。
二、启动流程拆解:先找到「吃时间」的元凶
我个人的习惯是,动手优化前先做一次完整的启动trace。你想想看,不知道时间花在哪,怎么优化?
2.1 启动阶段时间分布
我们用bootchart抓了一次完整的启动数据,结果如下:
| 启动阶段 | 优化前耗时 | 占比 | 主要瓶颈 |
|---|---|---|---|
| Bootloader + Kernel | 8.2s | 27.3% | 驱动加载、文件系统挂载 |
| Init 进程 | 6.5s | 21.7% | SELinux策略加载、服务串行启动 |
| Zygote + SystemServer | 9.8s | 32.7% | 类预加载、服务注册 |
| Launcher 启动 | 5.5s | 18.3% | 资源加载、权限检查 |
| 总计 | 30.0s | 100% |
看到没?Zygote和SystemServer占了将近三分之一。但最让我意外的是,Init阶段的SELinux策略加载居然花了2秒多。这个后面细说。
2.2 优化策略总览
我画了一张图,把整个优化思路串起来。你看一眼就明白了:
三、Kernel 层优化:砍掉不必要的驱动
Kernel 阶段花了8.2秒,这里面有一半是驱动加载。我记得当时打开内核配置一看,好家伙,一堆用不上的驱动都编译进去了。
3.1 驱动裁剪
我们做了三件事:
- 移除无用驱动:比如一些老旧的USB控制器、不支持的WiFi芯片驱动
- 驱动模块化:把不常用的驱动改成模块,按需加载
- 延迟初始化:非关键驱动放到系统启动后再加载
小技巧:用 lsmod 和 dmesg 配合,可以快速找出哪些驱动是「吃时间但不干活」的。我一般会在内核启动参数里加 initcall_debug,这样每个驱动的初始化时间都看得清清楚楚。
3.2 文件系统优化
文件系统挂载也花了将近1秒。我们做了两个改动:
- 把
system分区从 ext4 改成 f2fs,随机读写性能提升明显 - 精简了
fstab中的挂载选项,去掉不必要的检查
优化后,Kernel 阶段从8.2秒降到了5.1秒。嗯,开门红。
四、Init 进程优化:SELinux 策略调整是重头戏
Init 阶段花了6.5秒,其中SELinux策略加载占了2.3秒。说实话,这个数字让我有点吃惊。我原本以为SELinux的瓶颈在运行时,没想到加载阶段就这么重。
4.1 SELinux 策略裁剪
我们当时的策略文件有多大?你猜猜——编译后的二进制策略文件 sepolicy 足足有 8.5MB。每次加载都要解析、校验、安装,不慢才怪。
我带着团队做了一次全面的策略审计,砍掉了以下内容:
- 未使用的类型和属性:大概有30%的type定义从未被引用过
- 冗余的allow规则:很多规则是「允许A访问B」,但A和B根本不在同一个设备上
- 调试用的permissive域:发布版本里这些完全可以去掉
关键操作:使用 sesearch 和 seinfo 工具分析策略文件,找出所有「死代码」。我写了一个脚本,自动标记出从未被引用的type和attribute,然后逐一确认删除。
优化后,策略文件从8.5MB降到了4.2MB,加载时间从2.3秒降到了1.1秒。
4.2 服务并行化启动
Init 的另一个问题是服务串行启动。你看 init.rc 文件,很多服务都加了 class main,但启动顺序是线性的。
我们做了两件事:
- 把不依赖的服务拆到
class late_start,让它们晚点启动 - 在
init.rc中使用trigger和wait机制,实现部分服务的并行化
# 优化前:串行启动
on boot
start servicemanager
start surfaceflinger
start media
# 优化后:并行启动
on boot
start servicemanager
trigger parallel_services
on parallel_services
start surfaceflinger
start media
注意:并行化不是无脑并行。有些服务之间有依赖关系,比如 servicemanager 必须在其他服务之前启动。我建议你先用 init.rc 的 class 机制做一次依赖分析,再决定哪些可以并行。
Init 阶段从6.5秒降到了3.8秒。嗯,效果不错。
五、SystemServer 优化:砍掉「启动时不需要」的服务
SystemServer 是启动的大头,9.8秒。这里面的优化空间其实很大,但风险也高——毕竟系统服务出了问题,手机可能直接变砖。
5.1 服务懒加载
我仔细看了一遍 SystemServer 的启动代码,发现很多服务其实不需要在启动时就初始化。比如:
- 打印服务(PrintManager):用户不会一开机就打印东西
- VPN 服务:同样,开机时不需要
- 部分硬件服务:比如NFC、红外,这些可以等用户使用时再启动
我们把这类服务改成了「按需启动」——第一次被调用时才初始化。这个改动大概省了2秒。
5.2 预加载精简
Zygote 预加载的类太多了。我记得默认的 preloaded-classes 文件里有将近3000个类。但实际上,很多类在启动后很久才会用到。
我们用 strace 抓了一次启动过程中的类加载情况,发现只有约60%的预加载类被实际使用。剩下的40%——嗯,白加载了。
优化后,预加载类从3000个精简到1800个,Zygote 启动时间从3.5秒降到了2.1秒。
六、Launcher 优化:让用户「感觉」更快
Launcher 启动花了5.5秒,这里面有资源加载、权限检查、数据库初始化等。我们做了三件事:
- 资源预加载到内存:把 Launcher 常用的图标、壁纸提前加载到内存缓存
- 权限检查缓存:第一次检查后缓存结果,避免重复检查
- 冷启动优化:减少 Launcher 启动时的数据库查询次数
优化后,Launcher 启动时间从5.5秒降到了3.0秒。
七、SELinux 策略调整经验总结
这次优化中,SELinux 策略调整是最让我头疼的部分。我踩过不少坑,分享几个给你:
7.1 策略裁剪的「度」
我曾经有一次砍得太狠,把某个关键服务的allow规则删了,结果手机启动后直接黑屏。嗯,从那以后我学乖了——每次裁剪后都要做完整的回归测试。
我的做法:先在一个测试分支上做裁剪,然后用 audit2allow 工具抓取所有被拒绝的访问,逐一确认是否合理。确认无误后再合并到主分支。
7.2 不要动核心策略
像 unconfined 域、kernel 域这些核心策略,我建议你尽量别动。动了之后,轻则系统不稳定,重则直接无法启动。
7.3 善用 permissive 域
在调试阶段,可以把某些服务设为 permissive 域,这样即使有违规访问也不会被拒绝。但发布版本一定要改回 enforcing。
# 调试阶段:设为 permissive
permissive system_app;
# 发布版本:删除这行,恢复 enforcing
八、最终成果
经过一个月的优化,启动时间从30秒降到了15秒。具体数据如下:
| 启动阶段 | 优化前 | 优化后 | 节省时间 |
|---|---|---|---|
| Bootloader + Kernel | 8.2s | 5.1s | 3.1s |
| Init 进程 | 6.5s | 3.8s | 2.7s |
| Zygote + SystemServer | 9.8s | 5.2s | 4.6s |
| Launcher 启动 | 5.5s | 3.0s | 2.5s |
| 总计 | 30.0s | 17.1s | 12.9s |
等等,不是说15秒吗?怎么是17.1秒?
嗯,这里有个小插曲。我们后来又做了一轮微调——把一些非关键服务的启动时机进一步后移,再加上内核参数的调优,最终压到了14.8秒。老板很满意,用户反馈也好了很多。
核心心得:启动优化不是一锤子买卖,而是一个持续迭代的过程。每次优化后都要重新测量、分析、调整。尤其是SELinux策略,一定要在安全性和性能之间找到平衡点。
好了,这次实战案例就分享到这里。如果你也在做启动优化,希望这些经验能帮你少走一些弯路。