23. 包管理器的扩展:PackageManagerCompat、Archives、Split APK 的支持
聊到包管理器的扩展,我得先说说一个现实问题。Android 版本碎片化有多严重,大家心里都有数。你写一个 PackageManager 的调用,在 API 30 上跑得好好的,换到 API 23 直接给你抛个异常。嗯,这就是 PackageManagerCompat 存在的意义。
23.1 PackageManagerCompat:兼容层的艺术
我个人习惯把 PackageManagerCompat 看作是 Google 给开发者铺的一条「平滑过渡带」。它藏在 androidx.core.content.pm 包下,专门解决不同 API 版本下 PackageManager 行为不一致的问题。
举个例子,getPackageInfo() 这个方法。在 API 28 之前,你传个 GET_SIGNATURES 标志就能拿到签名信息。但从 API 28 开始,Google 废弃了这个标志,改用了 GET_SIGNING_CERTIFICATES。如果你直接写旧代码,在 Android 12 上可能拿不到任何签名数据。
// 不推荐:直接使用旧 API
PackageInfo info = packageManager.getPackageInfo(
packageName,
PackageManager.GET_SIGNATURES
);
// 推荐:使用 Compat 类
PackageInfo info = PackageManagerCompat.getPackageInfo(
packageManager,
packageName,
PackageManagerCompat.GET_SIGNATURES
);
我在项目中遇到过这样一个坑:某个安全 SDK 需要校验应用签名,团队里有人直接用了 GET_SIGNATURES。测试时用的都是 Android 10 以下的设备,没发现问题。结果上线后,用户反馈在 Android 12 上签名校验一直失败。排查了半天,最后换成 PackageManagerCompat 才搞定。
PackageManagerCompat自动处理 API 版本差异- 内部使用
ApiImplementation模式,按版本选择实现 - 推荐所有涉及
PackageManager的调用都走 Compat 层
23.2 Archives:APK 的「压缩包」新形态
Android 11 引入了 PackageInstaller.Archives 的概念。说白了,就是把多个 APK 打包成一个「归档文件」,方便批量安装或分发。你想想看,以前你要安装一个包含多个 Split APK 的应用,得一个一个传,多麻烦。
Archives 的格式其实就是一个 ZIP 包,里面包含了所有需要安装的 APK 文件。系统在解析时,会先解压这个归档,然后按照 Split APK 的规则去安装。
// 创建 Archives 的伪代码
File archiveFile = new File("app.archives");
try (ZipOutputStream zos = new ZipOutputStream(
new FileOutputStream(archiveFile))) {
// 添加 base APK
zos.putNextEntry(new ZipEntry("base.apk"));
// 写入 base APK 数据...
// 添加 split APK
zos.putNextEntry(new ZipEntry("config.arm64.apk"));
// 写入 split APK 数据...
}
我记得有一次做内部应用分发平台,需要支持批量安装。最开始我们用的是逐个 APK 安装的方式,结果用户反馈安装速度慢,而且容易出错。后来改用了 Archives 格式,把 base APK 和所有 Split APK 打包成一个文件,安装成功率直接提升到了 99% 以上。
.apks 或 .archives,系统都能识别。我个人习惯用 .apks,因为更直观。
23.3 Split APK 的支持:模块化安装的基石
Split APK 不是什么新鲜概念了,但它的支持机制一直在演进。从 Android 5.0 开始,Google 就允许一个应用由多个 APK 文件组成。每个 Split APK 负责一部分功能,比如不同 CPU 架构的 native 库、不同屏幕密度的资源、不同语言的字符串。
为什么需要 Split APK?说白了就是为了减少下载体积。你想想看,一个应用如果同时支持 ARM 和 x86 架构,把所有 native 库都塞进一个 APK 里,体积至少翻倍。而 Split APK 可以让用户只下载自己设备需要的部分。
| Split 类型 | 配置限定符 | 典型用途 |
|---|---|---|
| config.arm64 | CPU 架构 | ARM64 的 native 库 |
| config.hdpi | 屏幕密度 | hdpi 资源 |
| config.zh | 语言 | 中文字符串 |
| config.feature | 功能模块 | 按需加载的功能 |
在 PackageManager 层面,Split APK 的安装和普通 APK 没什么区别。系统会通过 PackageInstaller.Session 来管理多个 APK 的安装过程。关键点在于,所有 Split APK 必须共享同一个 packageName 和 versionCode,否则系统会拒绝安装。
// 使用 Session 安装 Split APK
PackageInstaller.Session session = packageInstaller.openSession(sessionParams);
// 添加 base APK
try (OutputStream out = session.openWrite("base.apk", 0, -1)) {
// 写入 base APK 数据
}
// 添加 split APK
try (OutputStream out = session.openWrite("config.arm64.apk", 0, -1)) {
// 写入 split APK 数据
}
// 提交安装
session.commit(statusReceiver);
splitName 不能重复,而且命名要遵循 config.<限定符> 的格式。如果命名不规范,系统在解析时可能会直接跳过该 Split APK,导致功能缺失。
23.4 核心逻辑流程图
下面这张图展示了 PackageManager 在处理 Split APK 和 Archives 时的核心流程。我把它画成了 SVG,方便你理解整个链路。
23.5 实际项目中的最佳实践
说了这么多理论,最后分享几个我在实际项目中的经验。
- 优先使用 Compat 层:所有
PackageManager的调用,能走 Compat 就走 Compat。别嫌麻烦,这能省掉你未来大量的兼容性 Bug。 - Split APK 的命名规范:
splitName一定要遵循config.<限定符>的格式。比如config.arm64、config.zh。命名不规范,系统不认。 - Archives 的压缩策略:打包 Archives 时,建议使用
ZipEntry.STORED模式(不压缩),因为 APK 本身已经是压缩过的。再压缩一次反而浪费时间。 - 测试覆盖所有 API 版本:我建议至少测试 API 23、API 28、API 30 和 API 33 这四个版本。这四个版本覆盖了大部分的行为变更点。
PackageManagerCompat 作为你的默认选择。它可能不是最炫酷的技术,但绝对是最稳妥的选择。
公众号:蓝海资料掘金营,微信deep3321