17. 包扫描与缓存:Boot-time scan、PackageCache、PackageSetting 的持久化

包扫描,说白了就是系统启动时,把 APK 文件一个个翻出来,读取里面的 AndroidManifest.xml,然后把信息整理成内存里的数据结构。这个过程听起来简单,但坑特别多。我早期做系统优化时,就遇到过开机速度被包扫描拖慢好几秒的情况。

17.1 Boot-time Scan:开机那场“大阅兵”

每次 Android 系统启动,PackageManagerService(简称 PKMS)都会执行一次完整的包扫描。它会遍历以下几个目录:

  • /system/framework:框架层的 jar 包
  • /system/app:系统预装应用
  • /system/priv-app:特权应用(权限更高)
  • /data/app:用户安装的应用
  • /data/app-private:受 DRM 保护的应用

扫描时,PKMS 会做三件事:

  1. 解析 APK:读取 AndroidManifest.xml,提取包名、版本号、权限、组件信息等。
  2. 校验签名:确保 APK 没有被篡改。
  3. 分配 UID:给每个应用分配一个唯一的用户 ID。

你想想看,如果手机里装了 300 个应用,每个 APK 都要解压、解析、校验,那开机时间肯定感人。我记得有一次,客户反馈某款低端机开机要 40 秒,查下来发现是包扫描占了 15 秒。

核心优化思路:Boot-time scan 只做“增量扫描”。系统会记录上次扫描后的变化,只处理新增或更新的 APK。这个机制叫“扫描缓存”。

17.2 PackageCache:内存里的“快递站”

扫描完成后,PKMS 会把解析结果存到 PackageCache 里。这是一个内存中的 HashMap,key 是包名,value 是 PackageParser.Package 对象。

为什么要搞个缓存?因为每次查询应用信息都去解析 APK 的话,性能会崩。PackageCache 的存在,说白了就是“一次解析,多次复用”。

// 伪代码示意
class PackageManagerService {
    final ArrayMap<String, PackageParser.Package> mPackages = new ArrayMap<>();

    Package getPackageInfo(String packageName) {
        return mPackages.get(packageName);
    }
}

PackageCache 里存了哪些东西?我列个表:

字段 说明
packageName 包名,唯一标识
versionCode 版本号,用于判断更新
applicationInfo 应用基本信息(图标、标签、主题等)
activities Activity 组件列表
services Service 组件列表
receivers BroadcastReceiver 列表
providers ContentProvider 列表
permissions 声明的权限列表
signatures 签名信息

嗯,这里要注意:PackageCache 是纯内存结构,一旦系统重启,它就没了。所以需要持久化。

17.3 PackageSetting 的持久化:把“记忆”写进磁盘

PackageSetting 是每个应用的“户口本”。它记录了应用的安装状态、权限授予情况、是否被禁用等信息。这些数据不能丢,所以 PKMS 会在以下时机写入磁盘:

  • 应用安装/卸载时
  • 权限变更时
  • 系统正常关机时
  • 定时刷写(防止意外断电丢失)

持久化文件主要存在 /data/system/packages.xml/data/system/packages.list 里。

packages.xml 是核心文件,结构类似这样:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<packages>
    <package name="com.example.app" codePath="/data/app/com.example.app-1">
        <sigs count="1">
            <cert index="0" key="308202..."/>
        </sigs>
        <perms>
            <item name="android.permission.INTERNET" granted="true"/>
        </perms>
    </package>
</packages>

我曾经遇到过一个问题:某次系统异常断电,packages.xml 文件损坏,导致重启后所有应用都丢失了权限信息。从那以后,我特别强调“写文件时必须用原子操作”——先写临时文件,再 rename 覆盖原文件。

避坑指南:千万不要直接修改 packages.xml!我曾经手贱改过一次,结果系统直接 boot loop。正确的做法是通过 PKMS 提供的 API 来操作。

17.4 核心流程图:从扫描到缓存再到持久化

下面这张图展示了整个流程。我习惯用 SVG 画这种逻辑图,清晰直观。

系统启动 Boot-time Scan(扫描所有 APK) 解析 AndroidManifest.xml PackageCache(内存缓存) 持久化到 packages.xml 持久化到 packages.list 虚线表示:定时刷写 / 正常关机时触发

17.5 增量扫描与缓存命中

每次开机都全量扫描?那太慢了。Android 从 8.0 开始引入了“增量扫描”机制。系统会记录每个 APK 的 lastModifiedTime,如果文件没变,就直接从缓存加载。

缓存命中流程:

  1. 检查 packages.xml 中是否有该包名的记录
  2. 对比 APK 文件的修改时间戳
  3. 如果一致,直接反序列化 PackageSetting 到内存
  4. 如果不一致,重新扫描 APK

个人经验:我建议在开发阶段,可以手动清除 /data/system/packages.xml 来强制重新扫描。但千万别在生产环境这么做,否则所有应用都会丢失权限状态。

17.6 性能优化:那些年我踩过的坑

包扫描的性能问题,我遇到过好几次。这里分享几个典型的优化点:

  • 并行扫描:多核 CPU 下,可以同时扫描多个 APK。但要注意 I/O 竞争,别让磁盘成为瓶颈。
  • 延迟加载:非核心应用可以放到后台扫描,先让系统启动完成。
  • 缓存预加载:把上次的 PackageCache 序列化到文件,下次开机直接加载,省去解析步骤。

嗯,其实还有一个容易被忽略的点:APK 的压缩方式。如果 APK 用了 stored 模式(不压缩),解析速度会快很多。但大部分市场下载的 APK 都是 deflate 压缩,解压会消耗 CPU。

我记得有一次,某款游戏 APK 有 200MB,里面资源文件全压缩了,解析它花了将近 2 秒。后来我们建议开发者把资源文件改成 stored 模式,解析时间降到了 0.3 秒。

17.7 总结一下

包扫描与缓存,说白了就是“空间换时间”的经典案例。Boot-time scan 负责把原始数据从磁盘读到内存,PackageCache 负责快速查询,PackageSetting 的持久化保证数据不丢失。

这三者配合好了,系统启动快、查询快、数据还安全。配合不好嘛……嗯,你可能就会遇到开机慢、应用闪退、权限丢失等各种问题。

我个人觉得,理解这个机制的关键在于“缓存一致性”。什么时候该刷新缓存?什么时候该写磁盘?这些决策直接影响系统的稳定性和性能。


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