一句话总结:
通过将原有的单线程整体扫描,重构为**“文件发现-并行解析-串行提交”**的三阶段生产者-消费者模型,我们可以在多核设备上显著提升启动时APK的解析速度,从而缩短开机时间。
1. 瓶颈分析:解构PMS扫描流程
传统PMS扫描之所以缓慢,是因为它将多个不同性质的任务在一个单线程中串行执行:
- 文件发现:遍历
/system、/data等目录,找出所有APK文件。 - 包解析(Package Parsing) :对每个APK进行I/O操作(读取文件)和CPU密集操作(解析Manifest、校验签名等)。这是主要的耗时瓶颈。
- 状态更新(Scanning &Commit) :将解析出的包信息更新到PMS的内部数据结构和配置文件(如
packages.xml)中。此过程涉及临界区资源访问,必须保证原子性和顺序性。
优化思路:保持“文件发现”和“状态更新”的串行特性,将中间的“包解析”阶段进行并行化处理。
2. 架构设计:分阶段的生产者-消费者模型
我们采用经典的生产者-消费者模型来解耦和并行化处理流程。
-
生产者(单线程文件发现器) :
- 职责:快速遍历APK目录,将
File对象作为任务,投入**“待解析任务队列” (BlockingQueue<File>)**。 - 优势:将耗时的解析操作分离,自身轻量且迅速。
- 职责:快速遍历APK目录,将
-
消费者集群(并行解析线程池) :
- 职责:从“待解析任务队列”中获取任务,执行
parsePackage,并将结果(ParseResult)放入**“已解析结果队列” (BlockingQueue<ParseResult>)**。 - 核心:线程池大小根据CPU核心数动态设定 (
Runtime.getRuntime().availableProcessors()),是实现并行加速的关键。
- 职责:从“待解析任务队列”中获取任务,执行
-
终结者(PMS主流程) :
- 职责:从“已解析结果队列”中获取结果,在**全局安装锁 (
mInstallLock)**的保护下,执行scanPackageTraced等串行化的状态更新操作。 - 保障:确保了对PMS核心数据结构访问的线程安全。
- 职责:从“已解析结果队列”中获取结果,在**全局安装锁 (
3. 核心实现代码
3.1 任务调度中心
public class ParallelPackageScanner {
private final int numThreads = Runtime.getRuntime().availableProcessors();
private final ExecutorService parserExecutor = Executors.newFixedThreadPool(numThreads);
private final BlockingQueue<File> pendingParseQueue = new LinkedBlockingQueue<>();
private final BlockingQueue<ParseResult> completedParseQueue = new LinkedBlockingQueue<>();
// 1. 生产者调用:提交APK文件进行解析
public void submitFile(File apkFile) {
pendingParseQueue.offer(apkFile);
}
// 2. 消费者线程池的工作单元
private void startWorkers() {
for (int i = 0; i < numThreads; i++) {
parserExecutor.submit(() -> {
while (true) {
try {
File apk = pendingParseQueue.take();
ParseResult result = parsePackage(apk); // 耗时操作
completedParseQueue.offer(result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
}
}
// 3. 终结者调用:获取一个已解析完成的包
public ParseResult takeResult() throws InterruptedException {
return completedParseQueue.take();
}
}
3.2 主流程集成(终结者逻辑)
void scanDirectory(File dir) {
// ... 生产者将文件放入 aScanner.submitFile() ...
for (int i = 0; i < totalFiles; i++) {
ParseResult result = aScanner.takeResult(); // 阻塞等待一个解析结果
synchronized (mInstallLock) { // 进入临界区,串行执行
if (result.pkg != null) {
scanPackageLI(result.pkg, ...);
} else {
handleParseError(...);
}
}
}
}
4. 性能与风险考量
4.1 智能负载均衡
为避免因文件大小不均导致部分线程提前空闲,生产者在提交任务前,可先对APK列表按文件大小降序排列,然后再依次提交。这样可以使大文件的解析任务尽早开始,提高线程池的整体利用率。
4.2 I/O瓶颈与线程数选择
在多核但I/O性能(闪存读写速度)一般的设备上,过多的解析线程可能因争抢I/O资源而互相等待,导致性能不升反降。
- 策略:可引入I/O密集度的考量,将线程数限制在一个合理的上限,例如
Math.min(cpuCores, 4),以寻求CPU并行化与I/O瓶颈之间的平衡。
4.3 异常处理与隔离
单个APK的解析失败不应中断整个扫描流程。
ParseResult中应包含错误信息。- “终结者”在拿到错误的
ParseResult后,记录日志、隔离损坏的APK(例如移动到特定目录),然后继续处理下一个结果,保证系统的健壮性。
5. 验证与测试
- 核心指标:开机后首次进入桌面的时间(
boot time),以及PackageManagerService相关的boot phase耗时。 - 基准测试:在相同设备上,对比优化前后的
Scan duration(dumpsys package timing)。 - 压力测试:构造包含大量(>1000个)大小不一(小、中、大)APK的测试场景,验证并行扫描的性能提升和稳定性。
- 回归测试:确保应用安装、卸载、更新、权限授予等所有PMS核心功能不受影响。