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 会做三件事:
- 解析 APK:读取 AndroidManifest.xml,提取包名、版本号、权限、组件信息等。
- 校验签名:确保 APK 没有被篡改。
- 分配 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 画这种逻辑图,清晰直观。
17.5 增量扫描与缓存命中
每次开机都全量扫描?那太慢了。Android 从 8.0 开始引入了“增量扫描”机制。系统会记录每个 APK 的 lastModifiedTime,如果文件没变,就直接从缓存加载。
缓存命中流程:
- 检查
packages.xml中是否有该包名的记录 - 对比 APK 文件的修改时间戳
- 如果一致,直接反序列化 PackageSetting 到内存
- 如果不一致,重新扫描 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