缩减 OTA 大小

本页介绍了为了减少不同 build 之间不必要的文件更改而对 AOSP 所做的更改。维护自己的构建系统的设备实现人员可参考此信息来缩减无线下载 (OTA) 更新的大小。

Android OTA 更新偶尔会包含与代码更改不对应的文件。它们实际上是构建系统工件。在不同时间、不同目录或不同机器上构建相同的代码时可能会发生上述情况,产生大量变更文件。此类多余的文件会增加 OTA 补丁的大小,因此很难确定更改的代码。

为了使 OTA 的内容更加透明,AOSP 包含旨在缩减 OTA 补丁大小的构建系统更改。已消除 build 之间的不必要的文件变更,而且只有补丁相关文件会包含在 OTA 更新中。AOSP 还包括 build diff 工具(用于滤除与 build 相关的常见文件变更,提供更简洁的 build 文件差异)和块映射工具(可帮助您确保块分配保持一致)。

构建系统可以通过多种方式创建不必要的大型补丁。为了缓解这个问题,在 Android 8.0 及更高版本中,我们实施了新功能,以缩减每个文件差异的补丁大小。缩减 OTA 更新软件包大小的改进包括:

  • 使用了 Brotli,Brotli 是一种通用、无损压缩算法,适用于非 A/B 设备更新的完整映像。您可以自定义 Brotli 来优化压缩。对于包括文件系统中的两个或更多块的较大更新(例如 system.img),设备制造商或合作伙伴可以添加自己的压缩算法,并且可以在相同更新的不同块中使用不同的压缩算法。
  • 使用了 Puffin 重新压缩工具,Puffin 是 deflate 流的确定性修补工具,用于处理 A/B OTA 更新生成的压缩和 diff 函数。
  • 更改了增量生成工具用法,例如如何使用 bsdiff 库来压缩补丁。在 Android 9 及更高版本中,bsdiff 工具会选择能够为补丁提供最佳压缩结果的压缩算法。
  • 改进了 update_engine,减少了将补丁应用到 A/B 设备更新时的内存消耗。
  • 改进了为基于块的 OTA 更新拆分大型 ZIP 文件的方式。imgdiff 中的模式会根据条目名称拆分过大的 APK 文件。与线性拆分文件以及使用 bsdiff 工具压缩文件相比,此命令会生成一个更小的补丁。

以下各部分讨论了影响 OTA 更新大小的各种问题、相应的解决方案以及 AOSP 中的实现示例。

文件顺序

问题:文件系统在请求目录中的文件列表时,并不保证文件顺序,尽管对于同一个检出,文件顺序通常是相同的。ls 等工具在默认情况下会对结果进行排序,但 findmake 等命令使用的通配符函数未排序。用户在使用这类工具之前,务必要对输出进行排序。

解决方案:如果您将 findmake 等工具与通配符函数搭配使用,请在使用这些命令的输出之前对其进行排序。在 Android.mk 文件中使用 $(wildcard)$(shell find) 时,请对其进行排序。有些工具(如 Java)会对输入进行排序,因此在对文件进行排序之前,请确认您使用的工具尚未执行此操作。

示例:很多实例使用内置 all-*-files-under 宏在核心构建系统中进行了修复,其中包括 all-cpp-files-under(因为有多项定义分布于其他 Makefile)。如需了解详情,请参阅以下内容:

build 目录

问题:变更构建内容所在的目录会导致二进制文件有所不同。Android build 中的大多数路径都是相对路径,因此 C/C++ 中的 __FILE__ 不是问题。不过,默认情况下调试符号会对完整的路径名进行编码,而对预剥离二进制文件进行哈希处理会生成 .note.gnu.build-id,因此调试符号变更会使二进制文件发生变化。

解决方案:AOSP 现在会使调试路径变成相对路径。有关详情,请参阅 CL:https://android.googlesource.com/platform/build/+/6a66a887baadc9eb3d0d60e26f748b8453e27a02

时间戳

问题:build 输出中的时间戳会导致不必要的文件变更。这可能会发生在以下位置:

  • C 或 C++ 代码中的 __DATE__/__TIME__/__TIMESTAMP__ 宏。
  • 基于 ZIP 的归档文件中嵌入的时间戳。

解决方案/示例:如需从 build 输出中移除时间戳,请按照下文 C/C++ 中的 __DATE__/__TIME__/__TIMESTAMP__归档文件中嵌入的时间戳中指定的说明操作。

C/C++ 中的 __DATE__/__TIME__/__TIMESTAMP__

这些宏始终针对不同 build 生成不同的输出,因此请勿使用。您可以使用以下几种方法来清除这些宏:

归档文件(zip、jar)中嵌入的时间戳

Android 7.0 通过将 -X 添加到 zip 命令的所有使用中,解决了 zip 归档文件中嵌入的时间戳的问题。这会从 zip 文件中移除构建器中的 UID/GID 和扩展的 Unix 时间戳。

新工具 ziptime(位于 /platform/build/+/master/tools/ziptime/)会重置 zip 头文件中的正常时间戳。有关详情,请参阅 README 文件

signapk 工具为 APK 文件设置的时间戳可能会因服务器所在的时区而异。有关详情,请参阅 CL:https://android.googlesource.com/platform/build/+/6c41036bcf35fe39162b50d27533f0f3bfab3028

版本字符串

问题:APK 版本字符串通常包含附加到硬编码版本的 BUILD_NUMBER。即使 APK 中并未发生任何变化,APK 也仍然会有所不同。

解决方案:从 APK 版本字符串中移除 build 号。

示例

启用设备上的 verity 计算功能

如果您在设备上启用了 dm-verity,OTA 工具就会自动选择您的 verity 配置,并启用设备上的 verity 计算功能。这样就可以在 Android 设备上计算 verity 块,而不是将 verity 块存储为 OTA 软件包中的原始字节。对于一个 2GB 分区,verity 块大约可使用 16MB 空间。

但是,在设备上计算 verity 可能需要很长时间。具体而言,运行前向纠错码可能需要很长时间。在 Pixel 设备上,这通常需要 10 分钟时间。在低端设备上,可能需要更长时间。如果您要停用设备上的 verity 计算功能,但仍需启用 dm-verity,那么您可以通过在生成 OTA 更新时将 --disable_fec_computation 传递给 ota_from_target_files 工具来实现停用。此标志会在 OTA 更新期间停用设备上的 verity 计算功能。这会减少 OTA 安装时间,但会增加 OTA 软件包的大小。如果您的设备未启用 dm-verity,传递此标志将不起作用。

一致的构建工具

问题:生成安装文件的工具必须一致(给定输入应始终生成相同的输出)。

解决方案/示例:以下构建工具需要进行变更:

使用 build diff 工具

由于有些情况下无法避免与 build 相关的文件变更,AOSP 添加了 build diff 工具 target_files_diff.py,可用于比较两个文件包。该工具会在两个 build 之间执行递归 diff,从而排除常见的 build 相关文件变更,例如:

  • build 输出中的预期变更(例如由于 build 号变更所导致)。
  • 由于当前构建系统中的已知问题所导致的变更。

若要使用 build diff 工具,请运行以下命令:

target_files_diff.py dir1 dir2

dir1dir2 是包含每个 build 的提取的目标文件的基础目录。

使块分配保持一致

对于给定的文件,尽管其内容在两个 build 之间会保持不变,但实际持有数据的块可能已发生变化。因此,更新程序必须执行不必要的 I/O 才能围绕 OTA 更新来移动块。

在虚拟 A/B OTA 更新中,不必要的 I/O 可大幅增加存储写入时快照所需的存储空间。在非 A/B OTA 更新中,由于块移动而需要增加更多 I/O,为 OTA 更新移动块会增加更新时间。

为了解决这个问题,Google 7.0 扩展了 make_ext4fs 工具,以确保各 build 之间的块分配保持一致。make_ext4fs 工具接受可选的 -d base_fs 标志,该标志可在生成 ext4 映像时尝试将文件分配到同一块。您可以从上一个 build 的目标文件的 zip 文件中提取块映射文件(例如 base_fs 映射文件)。对于每个 ext4 分区,IMAGES 目录中有 .map 文件(例如,IMAGES/system.map 对应于 system 分区)。然后,您可以通过 PRODUCT_<partition>_BASE_FS_PATH 签入和指定这些 base_fs 文件,如下例所示:

  PRODUCT_SYSTEM_BASE_FS_PATH := path/to/base_fs_files/base_system.map
  PRODUCT_SYSTEM_EXT_BASE_FS_PATH := path/to/base_fs_files/base_system_ext.map
  PRODUCT_VENDOR_BASE_FS_PATH := path/to/base_fs_files/base_vendor.map
  PRODUCT_PRODUCT_BASE_FS_PATH := path/to/base_fs_files/base_product.map
  PRODUCT_ODM_BASE_FS_PATH := path/to/base_fs_files/base_odm.map

虽然这不会有助于降低整体 OTA 软件包大小,但它确实可以通过减少 I/O 量来改善 OTA 更新性能。对于虚拟 A/B 更新,可以大大减少应用 OTA 所需的存储空间。

避免更新应用

除了最大限度减少 build diff 以外,对于通过应用商店获取更新的应用,您可以排除更新,从而减少 OTA 更新大小。APK 通常包含设备上各种分区的重要部分。在 OTA 更新中纳入由应用商店更新的应用的最新版本可能会对 OTA 软件包产生大大小影响,并且对用户几乎没有什么用处。当用户收到 OTA 软件包时,他们可能已经拥有更新的应用,甚至直接从应用商店收到更高版本。