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% 以上。

小技巧: Archives 文件的后缀名可以是 .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 必须共享同一个 packageNameversionCode,否则系统会拒绝安装。

// 使用 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);
避坑指南: 我曾经遇到过一个问题:Split APK 的 splitName 不能重复,而且命名要遵循 config.<限定符> 的格式。如果命名不规范,系统在解析时可能会直接跳过该 Split APK,导致功能缺失。

23.4 核心逻辑流程图

下面这张图展示了 PackageManager 在处理 Split APK 和 Archives 时的核心流程。我把它画成了 SVG,方便你理解整个链路。

PackageManager 扩展核心流程 输入:APK / Archives PackageManagerCompat 解析 是否 Split APK? Split APK 处理 按 splitName 分发 普通 APK 处理 直接安装 PackageInstaller.Session 安装 安装完成

23.5 实际项目中的最佳实践

说了这么多理论,最后分享几个我在实际项目中的经验。

  1. 优先使用 Compat 层:所有 PackageManager 的调用,能走 Compat 就走 Compat。别嫌麻烦,这能省掉你未来大量的兼容性 Bug。
  2. Split APK 的命名规范splitName 一定要遵循 config.<限定符> 的格式。比如 config.arm64config.zh。命名不规范,系统不认。
  3. Archives 的压缩策略:打包 Archives 时,建议使用 ZipEntry.STORED 模式(不压缩),因为 APK 本身已经是压缩过的。再压缩一次反而浪费时间。
  4. 测试覆盖所有 API 版本:我建议至少测试 API 23、API 28、API 30 和 API 33 这四个版本。这四个版本覆盖了大部分的行为变更点。
一句话总结: PackageManager 的扩展机制,本质上是为了解决「碎片化」和「模块化」这两个核心问题。Compat 层解决碎片化,Split APK 和 Archives 解决模块化。理解了这一点,你就能更好地驾驭 Android 的包管理生态。
个人建议: 如果你正在开发一个需要分发到多个 Android 版本的应用,不妨把 PackageManagerCompat 作为你的默认选择。它可能不是最炫酷的技术,但绝对是最稳妥的选择。

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