10、SystemServer启动优化:关键服务启动顺序调整与并行化策略
SystemServer,说白了就是Android系统的“大管家”。它里面跑着几十个核心服务——ActivityManager、WindowManager、PackageManager……这些服务谁先启动、谁后启动,直接决定了你开机进桌面的速度。
我个人习惯把SystemServer的启动过程比作“搭积木”。有些积木是地基,必须最先放好;有些积木是柱子,可以跟其他柱子一起立起来;还有些积木是装饰,晚点放也没关系。嗯,今天我们就聊聊怎么优化这个“搭积木”的过程。
10.1 关键服务启动顺序调整
先看一个典型问题。我在项目中遇到过,某款设备开机总是卡在“Android”logo处十几秒。抓了log一看,原来是PackageManagerService在扫描所有已安装应用,而它启动时,ActivityManagerService还没准备好——结果PMS一直在等AMS的信号,白白浪费了时间。
为什么会这样?因为SystemServer里服务的启动顺序,是写死在代码里的。我们来看一段典型的启动流程:
// SystemServer.java (简化版)
private void run() {
// 第一阶段:基础服务
startBootstrapServices(); // 启动引导服务
startCoreServices(); // 启动核心服务
startOtherServices(); // 启动其他服务
}
每个阶段内部,服务是按顺序启动的。比如startBootstrapServices()里:
private void startBootstrapServices() {
// 1. 先启动Installer
Installer installer = new Installer();
// 2. 再启动ActivityManagerService
mActivityManagerService = new ActivityManagerService(context);
// 3. 然后启动PowerManagerService
mPowerManagerService = new PowerManagerService(context);
// ...
}
这种串行启动方式,每个服务都要等前一个服务初始化完成才能开始。如果某个服务初始化耗时较长(比如PMS扫描应用),后面的服务就得干等着。
我建议的做法是:先梳理出服务之间的依赖关系图。比如:
| 服务名称 | 依赖的服务 | 被依赖的服务 |
|---|---|---|
| ActivityManagerService | Installer, PowerManagerService | PackageManagerService, WindowManagerService |
| PackageManagerService | Installer, ActivityManagerService | — |
| WindowManagerService | ActivityManagerService, DisplayManagerService | — |
| DisplayManagerService | PowerManagerService | WindowManagerService |
有了这张表,你就能清楚地知道:哪些服务可以提前启动,哪些必须等。
10.2 服务并行化启动策略
好,依赖关系理清了。接下来就是怎么并行。
Android从Android 10开始,引入了ServiceThreadPool机制。说白了,就是用一个线程池来并行启动那些没有依赖关系的服务。
举个例子,假设我们有三个服务:A、B、C。A依赖B,C不依赖任何人。传统做法是:A→B→C,串行。优化后:C和B同时启动,等B完成后,再启动A。
代码实现上,我习惯这样写:
// 使用CountDownLatch实现并行启动
final CountDownLatch latch = new CountDownLatch(2);
// 线程1:启动服务B
new Thread(() -> {
startServiceB();
latch.countDown();
}).start();
// 线程2:启动服务C
new Thread(() -> {
startServiceC();
latch.countDown();
}).start();
// 主线程等待B和C都完成
latch.await();
// 然后启动服务A(依赖B)
startServiceA();
你想想看,如果每个服务初始化需要200ms,三个串行就是600ms。并行后,B和C同时跑,只需要200ms,再加上A的200ms,总共400ms——省了200ms。
更高级的做法,是使用依赖注入框架来管理服务启动顺序。比如用Dagger或Hilt,把每个服务定义成一个“组件”,组件之间通过注解声明依赖关系。框架会自动计算出最优的启动顺序和并行策略。
不过说实话,对于大多数项目,手动调整启动顺序+简单的线程池并行,已经能解决80%的问题了。
10.3 SELinux策略对SystemServer的影响
嗯,这里要重点说一下SELinux。很多同学觉得SELinux只是安全模块,跟启动优化没关系。其实关系大了去了。
我在项目中遇到过这样一个问题:某款设备开机后,SystemServer启动到一半就崩溃了。抓了kernel log一看,是SELinux avc denial——SystemServer想访问某个文件,但SELinux策略不允许。
为什么会这样?因为SystemServer在启动过程中,需要访问大量系统文件、设备节点、socket等。如果SELinux策略没有为这些访问提前打好“通行证”,服务就会因为权限不足而启动失败。
具体来说,SELinux对SystemServer的影响主要体现在三个方面:
- 文件访问权限:SystemServer需要读取/system、/data等分区下的文件。如果SELinux策略没有为system_server域授予相应的读/写权限,服务就会卡住。
- 进程间通信(IPC):SystemServer中的服务之间通过Binder通信。如果SELinux策略没有允许system_server域与其他域(如untrusted_app域)进行Binder调用,服务就无法正常工作。
- 设备节点访问:某些服务需要访问硬件设备节点(如/dev/graphics/fb0)。如果SELinux策略没有为system_server域授予设备节点的访问权限,服务就会崩溃。
我建议的做法是:在开发阶段,先关闭SELinux(设置为permissive模式),观察SystemServer启动是否正常。如果正常,再逐步开启SELinux,根据avc denial日志,一条一条地添加策略规则。
下面是一个典型的SELinux策略规则示例:
# 允许system_server域读取/data/system目录下的文件
allow system_server system_data_file:dir { read search };
allow system_server system_data_file:file { read open getattr };
# 允许system_server域与untrusted_app域进行Binder通信
allow system_server untrusted_app:unix_stream_socket { connectto };
allow system_server untrusted_app:binder { call transfer };
# 允许system_server域访问framebuffer设备
allow system_server graphics_device:chr_file { read write open };
你可能会问:这些规则怎么调试?我一般用audit2allow工具。把avc denial日志丢进去,它自动生成对应的allow规则。不过要注意,自动生成的规则可能过于宽松,需要人工审核一下。
另外,SELinux策略的加载时机也很关键。SystemServer启动时,SELinux策略必须已经加载完毕。如果策略加载晚了,SystemServer在启动过程中就会因为权限不足而失败。所以,SELinux策略的加载通常放在init进程中,在SystemServer启动之前完成。
10.4 知识体系总览
下面这张图,把本章的核心逻辑串起来了:
这张图展示了三个优化方向之间的关系。启动顺序调整是基础,并行化是加速手段,SELinux策略是保障。三者缺一不可。
好了,关于SystemServer启动优化,核心内容就是这些。记住一句话:没有银弹。每个项目的优化方案都不一样,关键是要理解原理,然后根据实际情况去调整。