此页面介绍了添加到 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
之类的工具默认会对结果进行排序,但是诸如 find
和 make
之类的命令使用的通配符函数不会进行排序。在使用这些工具之前,您必须对输出进行排序。
解决方案:当您将 find
和 make
等工具与通配符函数结合使用时,请在使用这些命令之前对其输出进行排序。在 Android.mk
文件中使用 $(wildcard)
或 $(shell find)
时,也要对其进行排序。某些工具(例如 Java)确实会对输入进行排序,因此在对文件进行排序之前,请确认您使用的工具尚未执行此操作。
示例:核心 build 系统中修复了许多实例,方法是使用内置的 all-*-files-under
宏,其中包括 all-cpp-files-under
(因为多个定义分散在其他 makefile 中)。有关详情,请参阅以下内容:
- https://android.googlesource.com/platform/build/+/4d66adfd0e6d599d8502007e4ea9aaf82e95569f
- https://android.googlesource.com/platform/build/+/379f9f9cec4fe1c66b6d60a6c19fecb81b9eb410
- https://android.googlesource.com/platform/build/+/7c3e3f8314eec2c053012dd97d2ae649ebeb5653
- https://android.googlesource.com/platform/build/+/5c64b4e81c1331cab56d8a8c201f26bb263b630c
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 生成不同的输出,因此请勿使用它们。以下是消除这些宏的几种选项:
- 移除它们。有关示例,请参阅 https://android.googlesource.com/platform/system/core/+/30622bbb209db187f6851e4cf0cdaa147c2fca9f。
- 要唯一标识正在运行的二进制文件,请从 ELF 标头中读取 build-id。
- 要了解操作系统是何时构建的,请读取
ro.build.date
(这适用于除增量 build 之外的所有内容,增量 build 可能不会更新此日期)。有关示例,请参阅 https://android.googlesource.com/platform/external/libchrome/+/8b7977eccc94f6b3a3896cd13b4aeacbfa1e0f84。
归档文件中嵌入的时间戳(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 编号。
示例
- https://android.googlesource.com/platform/packages/apps/Camera2/+/5e0f4cf699a4c7c95e2c38ae3babe6f20c258d27
- https://android.googlesource.com/platform/build/+/d75d893da8f97a5c7781142aaa7a16cf1dbb669c
启用设备上完整性计算
如果在您的设备上启用了 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 工具需要进行更改:
- NOTICE 文件创建器。NOTICE 文件创建器已更改为创建可重现的 NOTICE 合集。请参阅 CL:https://android.googlesource.com/platform/build/+/8ae4984c2c8009e7a08e2a76b1762c2837ad4f64。
- Java Android Compiler Kit (Jack)。Jack 工具链需要更新以处理生成的构造函数排序中的偶尔更改。为构造函数添加了确定性访问器到工具链:https://android.googlesource.com/toolchain/jack/+/056a5425b3ef57935206c19ecb198a89221ca64b。
- ART AOT 编译器 (dex2oat)。ART 编译器二进制文件收到了更新,添加了用于创建确定性映像的选项:https://android.googlesource.com/platform/art/+/ace0dc1dd5480ad458e622085e51583653853fb9。
-
libpac.so 文件 (V8)。每个 build 都会创建一个不同的
/system/lib/libpac.so
文件,因为 V8 快照会针对每个 build 发生更改。解决方案是移除快照:https://android.googlesource.com/platform/external/v8/+/e537f38c36600fd0f3026adba6b3f4cbcee1fb29。 - 应用预先 dexopt (.odex) 文件。预先 dexopt (.odex) 文件在 64 位系统上包含未初始化的填充。这已得到纠正:https://android.googlesource.com/platform/art/+/34ed3afc41820c72a3c0ab9770be66b6668aa029。
使用 build diff 工具
对于无法消除 build 相关文件变更的情况,AOSP 包含一个 build diff 工具 target_files_diff.py
,用于比较两个文件软件包。此工具在两个 build 之间执行递归差异比较,但不包括常见的 build 相关文件变更,例如:
- build 输出中的预期变更(例如,由于 build 编号变更)。
- 由于当前 build 系统中的已知问题而导致的变更。
要使用 build diff 工具,请运行以下命令:
target_files_diff.py dir1 dir2
dir1
和 dir2
是基本目录,其中包含每个 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 软件包时,他们可能已经拥有从应用商店直接收到的更新应用,甚至是更新的版本。