减少 OTA 大小

此页面介绍了添加到 AOSP 的变更,以减少 build 之间不必要的文件变更。维护自己的 build 系统的设备实现者可以使用此信息作为指南,来减小其无线下载 (OTA) 更新的大小。

Android OTA 更新偶尔会包含已更改的文件,但这些文件与代码变更并不对应。它们实际上是 build 系统工件。当在不同时间、从不同目录或在不同机器上构建同一代码时,可能会生成大量已更改的文件。此类多余的文件会增加 OTA 补丁的大小,并且难以确定哪些代码已更改。

为了使 OTA 的内容更加透明,AOSP 包含旨在减小 OTA 补丁大小的 build 系统变更。build 之间不必要的文件变更已消除,OTA 更新中仅包含与补丁相关的文件。AOSP 还包含一个 build diff 工具,该工具可以滤除常见的 build 相关文件变更,以提供更简洁的 build 文件差异,以及一个 块映射工具,该工具可帮助您保持块分配的一致性。

build 系统可能会通过多种方式创建过大的补丁。为了缓解这种情况,在 Android 8.0 及更高版本中,我们实现了一些新功能来减小每个文件差异的补丁大小。减小 OTA 更新软件包大小的改进包括以下各项:

  • 在非 A/B 设备更新的完整映像上使用通用无损压缩算法 ZSTD。通过提高压缩级别,可以自定义 ZSTD 以获得更高的压缩率。压缩级别在 OTA 生成时设置,可以通过传递标志 --vabc_compression_param=zstd,$COMPRESSION_LEVEL 进行设置
  • 增加 OTA 期间使用的压缩窗口大小。可以通过自定义设备 .mk 文件中的 build 参数来设置最大压缩窗口大小。此变量设置为 PRODUCT_VIRTUAL_AB_COMPRESSION_FACTOR := 262144
  • 使用 Puffin 重新压缩,这是一种用于 deflate 流的确定性补丁工具,可处理 A/B OTA 更新生成的压缩和差异函数。
  • 更改了增量生成工具的用法,例如 bsdiff 库如何用于压缩补丁。在 Android 9 及更高版本中,bsdiff 工具会选择压缩算法,以便为补丁提供最佳压缩效果。
  • update_engine 的改进减少了为 A/B 设备更新应用补丁时消耗的内存。

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

文件顺序

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

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

示例:核心 build 系统中修复了许多实例,方法是使用内置的 all-*-files-under 宏,其中包括 all-cpp-files-under(因为多个定义分散在其他 makefile 中)。有关详情,请参阅以下内容:

Build 目录

问题:更改事物的 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 通过向 zip 命令的所有用法添加 -X,修复了 zip 归档文件中嵌入的时间戳问题。这从 zip 文件中移除了构建器的 UID/GID 和扩展的 Unix 时间戳。

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

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

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

版本字符串

问题:APK 版本字符串通常会在其硬编码版本后附加 BUILD_NUMBER。即使 APK 中没有其他任何更改,结果 APK 仍然会有所不同。

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

示例

启用设备上完整性计算

如果在您的设备上启用了 dm-verity,则 OTA 工具会自动拾取您的完整性配置,并启用设备上完整性计算。这样一来,就可以在 Android 设备上计算完整性块,而不是将其作为原始字节存储在您的 OTA 软件包中。对于 2GB 分区,完整性块可以使用大约 16MB。

但是,在设备上计算完整性可能需要很长时间。具体来说,前向纠错代码可能需要很长时间。在 Pixel 设备上,这往往需要长达 10 分钟。在低端设备上,这可能需要更长时间。如果您想要停用设备上完整性计算,但仍然启用 dm-verity,则可以在生成 OTA 更新时,通过将 --disable_fec_computation 传递给 ota_from_target_files 工具来执行此操作。此标志会在 OTA 更新期间停用设备上完整性计算。它可以缩短 OTA 安装时间,但会增加 OTA 软件包大小。如果您的设备未启用 dm-verity,则传递此标志无效。

一致的 build 工具

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

解决方案/示例:以下 build 工具需要进行更改:

使用 build diff 工具

对于无法消除 build 相关文件变更的情况,AOSP 包含一个 build diff 工具 target_files_diff.py,用于比较两个文件软件包。此工具在两个 build 之间执行递归差异比较,但不包括常见的 build 相关文件变更,例如:

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

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

target_files_diff.py dir1 dir2

dir1dir2 是基本目录,其中包含每个 build 的提取目标文件。

保持块分配一致

对于给定文件,虽然其内容在两个 build 之间保持不变,但保存数据的实际块可能已更改。因此,更新程序必须执行不必要的 I/O,以在 OTA 更新中移动块。

在 Virtual A/B OTA 更新中,不必要的 I/O 会大大增加存储复制时写入快照所需的存储空间。在非 A/B OTA 更新中,由于块移动导致更多 I/O,因此移动 OTA 更新的块会增加更新时间。

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

  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 更新性能。对于 Virtual A/B 更新,它可以大大减少应用 OTA 所需的存储空间。

避免更新应用

除了最大限度地减少 build 差异之外,您还可以通过排除通过应用商店获取更新的应用的更新来减小 OTA 更新大小。APK 通常构成设备上各种分区的重要组成部分。在 OTA 更新中包含通过应用商店更新的应用的最新版本可能会对 OTA 软件包的大小产生很大影响,并且几乎不会给用户带来好处。当用户收到 OTA 软件包时,他们可能已经拥有从应用商店直接收到的更新应用,甚至是更新的版本。