一、从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 优化 • 驱动延迟加载 • 文件系统精简 • initramfs 压缩 Init 进程优化 • 并行化服务启动 • SELinux 策略裁剪 • rc 文件合并 SystemServer 优化 • 服务懒加载 • 预加载精简 • Binder 优化 Launcher 优化 • 资源预加载 • 权限缓存 • 冷启动优化 优化结果:30秒 → 15秒 SELinux 策略调整 + 并行化 + 资源裁剪 每个优化方向都贡献了 3~5 秒的加速,SELinux 策略调整是其中最关键的一环

三、Kernel 层优化:砍掉不必要的驱动

Kernel 阶段花了8.2秒,这里面有一半是驱动加载。我记得当时打开内核配置一看,好家伙,一堆用不上的驱动都编译进去了。

3.1 驱动裁剪

我们做了三件事:

  • 移除无用驱动:比如一些老旧的USB控制器、不支持的WiFi芯片驱动
  • 驱动模块化:把不常用的驱动改成模块,按需加载
  • 延迟初始化:非关键驱动放到系统启动后再加载

小技巧:lsmoddmesg 配合,可以快速找出哪些驱动是「吃时间但不干活」的。我一般会在内核启动参数里加 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域:发布版本里这些完全可以去掉

关键操作:使用 sesearchseinfo 工具分析策略文件,找出所有「死代码」。我写了一个脚本,自动标记出从未被引用的type和attribute,然后逐一确认删除。

优化后,策略文件从8.5MB降到了4.2MB,加载时间从2.3秒降到了1.1秒。

4.2 服务并行化启动

Init 的另一个问题是服务串行启动。你看 init.rc 文件,很多服务都加了 class main,但启动顺序是线性的。

我们做了两件事:

  • 把不依赖的服务拆到 class late_start,让它们晚点启动
  • init.rc 中使用 triggerwait 机制,实现部分服务的并行化
# 优化前:串行启动
on boot
    start servicemanager
    start surfaceflinger
    start media

# 优化后:并行启动
on boot
    start servicemanager
    trigger parallel_services

on parallel_services
    start surfaceflinger
    start media

注意:并行化不是无脑并行。有些服务之间有依赖关系,比如 servicemanager 必须在其他服务之前启动。我建议你先用 init.rcclass 机制做一次依赖分析,再决定哪些可以并行。

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策略,一定要在安全性和性能之间找到平衡点。

好了,这次实战案例就分享到这里。如果你也在做启动优化,希望这些经验能帮你少走一些弯路。

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