24. 性能优化:PMS 启动速度优化、延迟扫描、增量编译策略
说到 PMS 的性能优化,我其实挺有感触的。早期做系统开发时,每次手机开机都要等好久,尤其是装了大量应用之后。你想想看,几百个 APK 要解析、扫描、校验,这个过程如果处理不好,用户体验会非常糟糕。今天我们就来聊聊 PMS 启动速度优化的三板斧:延迟扫描、增量编译,以及一些我踩过的坑。
24.1 PMS 启动慢的根源在哪?
先搞清楚问题在哪。PMS 启动慢,说白了就是它要做的事情太多了。我把它拆解成三个主要阶段:
- 扫描阶段:遍历所有应用目录,找到每个 APK 文件
- 解析阶段:读取 AndroidManifest.xml,提取包名、组件、权限等信息
- 校验阶段:验证签名、检查包完整性、处理 sharedUserId 冲突
这三个阶段是串行执行的。如果某个 APK 特别大,或者签名校验特别慢,整个启动流程就会被拖住。我记得有一次,客户反馈某款手机开机要等 40 秒,查了半天发现是一个 200MB 的游戏 APK 签名校验花了 8 秒。嗯,这就是典型的性能瓶颈。
核心矛盾:PMS 需要在开机时完成所有应用的扫描和校验,但用户希望手机能尽快进入桌面。这两者天然冲突。
24.2 延迟扫描:把非必要工作往后推
延迟扫描的思路其实很简单:不是所有应用都需要在开机时立即准备好。系统应用、核心服务必须马上可用,但第三方应用完全可以等用户用到时再加载。
我参与过一个项目,把 PMS 的扫描分成了两个阶段:
- 第一阶段(必须):扫描 /system/app 和 /system/priv-app,保证系统能正常运行
- 第二阶段(延迟):扫描 /data/app,等系统启动完成后再慢慢处理
具体实现上,Android 从 8.0 开始引入了 PackageManagerService.ScanOnly 模式。这个模式下,PMS 只做轻量级的包信息读取,不做完整的签名校验和 dex 优化。等到用户真正要启动某个应用时,再触发完整的扫描流程。
// 延迟扫描的核心逻辑(简化版)
private void scanPackageOnly(PackageParser.Package pkg, int flags) {
// 只解析 AndroidManifest.xml 的基本信息
pkg.setScanOnly(true);
// 跳过签名校验
// 跳过 dexopt 优化
// 跳过 sharedUserId 校验
// 记录到缓存,但不写入 packages.xml
mPackages.put(pkg.packageName, pkg);
}
// 当用户点击应用图标时,触发完整扫描
private void ensurePackageScanned(String packageName) {
PackageParser.Package pkg = mPackages.get(packageName);
if (pkg != null && pkg.isScanOnly()) {
// 执行完整的扫描流程
performFullScan(pkg);
}
}
我的经验:延迟扫描能减少 30%-50% 的 PMS 启动时间。但要注意,如果用户频繁启动未扫描的应用,反而会因为反复触发完整扫描而变慢。所以需要配合预加载策略,比如在系统空闲时主动扫描常用应用。
24.3 增量编译:别每次都从头来
增量编译是另一个让我印象深刻的优化点。早期 Android 版本中,每次系统升级或者应用更新,PMS 都会重新编译所有应用的 dex 文件。这简直是噩梦——尤其是当你有 200 个应用时,编译时间可能长达 10 分钟。
增量编译的思路是:只编译发生变化的部分。比如某个应用只更新了一个资源文件,那就不需要重新编译它的 dex 代码。PMS 通过比较文件的 lastModifiedTime 和 crc32 来判断是否需要重新编译。
// 增量编译的判断逻辑
private boolean needsDexopt(String packageName, File apkFile) {
// 获取上次编译的时间戳
long lastCompiledTime = getLastCompiledTime(packageName);
long apkModifiedTime = apkFile.lastModified();
// 如果 APK 没有变化,跳过编译
if (apkModifiedTime <= lastCompiledTime) {
return false;
}
// 检查 dex 文件的 CRC 是否变化
long currentCrc = calculateCrc(apkFile);
long lastCrc = getLastCrc(packageName);
return currentCrc != lastCrc;
}
这里有个坑,我曾经踩过。有些应用会频繁修改 APK 文件的修改时间,但实际上内容没变。这会导致 PMS 每次都认为需要重新编译。解决方案是:不要只依赖文件时间戳,要结合 CRC 校验。虽然 CRC 计算本身有开销,但比起全量编译,这点代价完全可以接受。
注意:增量编译不是万能的。如果系统运行时(runtime)发生了变化,比如 ART 虚拟机升级了,那所有应用都需要重新编译。这种情况下,增量编译退化为全量编译。
24.4 知识体系:PMS 启动优化全景图
下面这张图是我自己总结的 PMS 启动优化知识体系。你可以看到,优化不是单点作战,而是从扫描、编译、缓存三个维度同时发力。
24.5 实战中的避坑指南
优化 PMS 启动速度,光知道理论是不够的。我把自己踩过的坑整理了一下,希望能帮你少走弯路。
| 坑点 | 现象 | 解决方案 |
|---|---|---|
| 延迟扫描导致应用图标显示异常 | 桌面图标出现灰色占位符 | 在扫描第一阶段预加载图标信息到内存缓存 |
| 增量编译判断失误 | 应用更新后功能异常 | 增加强制全量编译的触发条件(如版本号变化) |
| 缓存文件损坏 | PMS 启动时崩溃 | 增加缓存校验机制,损坏时自动重建 |
| 多用户场景下的扫描冲突 | 第二个用户登录时 PMS 卡死 | 为每个用户维护独立的扫描队列 |
我曾经遇到过一个问题:某款手机在升级系统后,PMS 启动时间从 3 秒暴涨到 15 秒。查了半天发现是增量编译的 CRC 计算逻辑有 bug,导致所有应用都被判定为「需要重新编译」。修复后,启动时间回到了 2.8 秒。所以,一定要给增量编译加上监控日志,方便排查问题。
24.6 总结一下
PMS 启动速度优化,说白了就是三个字:少干活。能延迟的就延迟,能复用的就复用,能跳过的就跳过。但要注意,优化不能牺牲稳定性。我个人的习惯是:先做性能基线测试,然后逐步优化,每一步都要验证功能是否正常。
嗯,关于 PMS 启动优化,今天就聊到这里。如果你在实际项目中遇到了其他坑,欢迎一起交流。