24. 性能优化:PMS 启动速度优化、延迟扫描、增量编译策略

说到 PMS 的性能优化,我其实挺有感触的。早期做系统开发时,每次手机开机都要等好久,尤其是装了大量应用之后。你想想看,几百个 APK 要解析、扫描、校验,这个过程如果处理不好,用户体验会非常糟糕。今天我们就来聊聊 PMS 启动速度优化的三板斧:延迟扫描、增量编译,以及一些我踩过的坑。

24.1 PMS 启动慢的根源在哪?

先搞清楚问题在哪。PMS 启动慢,说白了就是它要做的事情太多了。我把它拆解成三个主要阶段:

  • 扫描阶段:遍历所有应用目录,找到每个 APK 文件
  • 解析阶段:读取 AndroidManifest.xml,提取包名、组件、权限等信息
  • 校验阶段:验证签名、检查包完整性、处理 sharedUserId 冲突

这三个阶段是串行执行的。如果某个 APK 特别大,或者签名校验特别慢,整个启动流程就会被拖住。我记得有一次,客户反馈某款手机开机要等 40 秒,查了半天发现是一个 200MB 的游戏 APK 签名校验花了 8 秒。嗯,这就是典型的性能瓶颈。

核心矛盾:PMS 需要在开机时完成所有应用的扫描和校验,但用户希望手机能尽快进入桌面。这两者天然冲突。

24.2 延迟扫描:把非必要工作往后推

延迟扫描的思路其实很简单:不是所有应用都需要在开机时立即准备好。系统应用、核心服务必须马上可用,但第三方应用完全可以等用户用到时再加载。

我参与过一个项目,把 PMS 的扫描分成了两个阶段:

  1. 第一阶段(必须):扫描 /system/app 和 /system/priv-app,保证系统能正常运行
  2. 第二阶段(延迟):扫描 /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 通过比较文件的 lastModifiedTimecrc32 来判断是否需要重新编译。

// 增量编译的判断逻辑
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 启动优化知识体系。你可以看到,优化不是单点作战,而是从扫描、编译、缓存三个维度同时发力。

PMS 启动优化全景图 扫描优化 编译优化 缓存优化 延迟扫描 分阶段扫描 跳过非核心应用 增量编译 CRC + 时间戳判断 后台空闲编译 packages.xml 缓存 dex 文件缓存 签名信息缓存 目标:将 PMS 启动时间控制在 2 秒以内 延迟扫描 + 增量编译 + 缓存命中率 > 90%

24.5 实战中的避坑指南

优化 PMS 启动速度,光知道理论是不够的。我把自己踩过的坑整理了一下,希望能帮你少走弯路。

坑点 现象 解决方案
延迟扫描导致应用图标显示异常 桌面图标出现灰色占位符 在扫描第一阶段预加载图标信息到内存缓存
增量编译判断失误 应用更新后功能异常 增加强制全量编译的触发条件(如版本号变化)
缓存文件损坏 PMS 启动时崩溃 增加缓存校验机制,损坏时自动重建
多用户场景下的扫描冲突 第二个用户登录时 PMS 卡死 为每个用户维护独立的扫描队列

我曾经遇到过一个问题:某款手机在升级系统后,PMS 启动时间从 3 秒暴涨到 15 秒。查了半天发现是增量编译的 CRC 计算逻辑有 bug,导致所有应用都被判定为「需要重新编译」。修复后,启动时间回到了 2.8 秒。所以,一定要给增量编译加上监控日志,方便排查问题。

24.6 总结一下

PMS 启动速度优化,说白了就是三个字:少干活。能延迟的就延迟,能复用的就复用,能跳过的就跳过。但要注意,优化不能牺牲稳定性。我个人的习惯是:先做性能基线测试,然后逐步优化,每一步都要验证功能是否正常。

嗯,关于 PMS 启动优化,今天就聊到这里。如果你在实际项目中遇到了其他坑,欢迎一起交流。


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