本頁介紹了添加到 AOSP 以減少構建之間不必要的文件更改的更改。維護自己的構建系統的設備實施者可以將此信息用作減少其無線 (OTA) 更新大小的指南。
Android OTA 更新有時會包含與代碼更改不對應的更改文件。它們實際上是構建系統工件。當在不同時間、不同目錄或不同機器上構建的相同代碼生成大量更改文件時,可能會發生這種情況。此類多餘的文件會增加 OTA 補丁的大小,並且難以確定更改了哪些代碼。
為了使 OTA 的內容更加透明,AOSP 包括旨在減少 OTA 補丁大小的構建系統更改。版本之間不必要的文件更改已被消除,OTA 更新中僅包含與補丁相關的文件。 AOSP 還包括一個構建差異工具,它可以過濾掉與構建相關的常見文件更改以提供更清晰的構建文件差異,以及一個塊映射工具,它可以幫助您保持塊分配的一致性。
構建系統可以通過多種方式創建不必要的大補丁。為了緩解這種情況,在 Android 8.0 及更高版本中,實施了新功能以減少每個文件差異的補丁大小。減少 OTA 更新包大小的改進包括以下內容:
- Brotli的使用,這是一種通用的無損壓縮算法,用於非 A/B 設備更新上的完整圖像。 Brotli可以定制以優化壓縮。在由文件系統中的兩個或多個塊組成的較大更新(例如
system.img
)中,設備製造商或合作夥伴可以添加自己的壓縮算法,並且可以對同一更新的不同塊使用不同的壓縮算法。 - 使用Puffin重新壓縮(一種用於放氣流的確定性修補工具)處理 A/B OTA 更新生成的壓縮和差異函數。
- 更改增量生成工具的使用,例如如何使用
bsdiff
庫來壓縮補丁。在 Android 9 及更高版本中,bsdiff
工具會選擇可為補丁提供最佳壓縮結果的壓縮算法。 - 對
update_engine
的改進導致在為 A/B 設備更新應用補丁時消耗的內存更少。 - 改進了拆分大型 zip 文件以進行基於塊的 OTA 更新。
imgdiff
中的一種模式根據條目名稱拆分超大的 APK 文件。與線性分割文件並使用bsdiff
工具壓縮它們相比,這會產生更小的補丁。
以下部分討論了影響 OTA 更新大小的各種問題、它們的解決方案以及 AOSP 中的實施示例。
文件順序
問題:當被要求提供目錄中的文件列表時,文件系統不保證文件順序,儘管對於相同的檢出通常是相同的。 ls
等工具默認對結果進行排序,但find
和make
等命令使用的通配符函數不排序。在使用這些工具之前,您必須對輸出進行排序。
解決方案:當您使用帶有通配符功能的find
和make
等工具時,請在使用之前對這些命令的輸出進行排序。在Android.mk
文件中使用$(wildcard)
或$(shell find)
時,也要對它們進行排序。某些工具(例如 Java)會對輸入進行排序,因此在對文件進行排序之前,請確認您正在使用的工具尚未這樣做。
示例:在核心構建系統中使用內置的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
構建目錄
問題:更改構建內容的目錄可能會導致二進製文件不同。 Android 構建中的大多數路徑都是相對路徑,因此 C/C++ 中的__FILE__
不是問題。但是,調試符號默認編碼完整的路徑名,並且.note.gnu.build-id
是通過散列預先剝離的二進製文件生成的,因此如果調試符號發生更改,它將更改。
解決方案: AOSP 現在使調試路徑相對。詳情參考CL: https ://android.googlesource.com/platform/build/+/6a66a887baadc9eb3d0d60e26f748b8453e27a02。
時間戳
問題:構建輸出中的時間戳會導致不必要的文件更改。這很可能發生在以下位置:
- C 或 C++ 代碼中的
__DATE__/__TIME__/__TIMESTAMP__
宏。 - 嵌入在基於 zip 的檔案中的時間戳。
解決方案/示例:要從構建輸出中刪除時間戳,請使用以下C/C++ 中 __DATE__/__TIME__/__TIMESTAMP__ 中給出的說明。和檔案中的嵌入時間戳。
__DATE__/__TIME__/__TIMESTAMP__ 在 C/C++ 中
這些宏總是為不同的構建產生不同的輸出,所以不要使用它們。以下是消除這些宏的一些選項:
- 刪除它們。例如,請參閱https://android.googlesource.com/platform/system/core/+/30622bbb209db187f6851e4cf0cdaa147c2fca9f 。
- 要唯一標識正在運行的二進製文件,請從 ELF 標頭中讀取 build-id。
- 要了解操作系統的構建時間,請閱讀
ro.build.date
(這適用於除增量構建之外的所有內容,增量構建可能不會更新此日期)。例如,請參閱https://android.googlesource.com/platform/external/libchrome/+/8b7977eccc94f6b3a3896cd13b4aeacbfa1e0f84 。
檔案中的嵌入時間戳(zip、jar)
Android 7.0 通過在zip
命令的所有使用中添加-X
來解決 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 版本字符串中刪除內部版本號。
解決方案:從 APK 版本字符串中刪除內部版本號。
例子:
- https://android.googlesource.com/platform/packages/apps/Camera2/+/5e0f4cf699a4c7c95e2c38ae3babe6f20c258d27
- https://android.googlesource.com/platform/build/+/d75d893da8f97a5c7781142aaa7a16cf1dbb669c
啟用設備端驗證計算
如果您的設備上啟用了dm-verity ,那麼 OTA 工具會自動獲取您的驗證配置,並啟用設備上驗證計算。這允許在 android 設備上計算驗證塊,而不是作為原始字節存儲在 OTA 包中。對於 2GB 的分區,Verity 塊可以使用大約 16MB。
但是,在設備上計算真實性可能需要很長時間。具體來說,前向糾錯碼可能需要很長時間。在像素設備上,這往往需要長達 10 分鐘。在低端設備上可能需要更長的時間。如果您想禁用設備端驗證計算,但仍啟用 dm-verity,您可以通過在生成 OTA 更新時將--disable_fec_computation
傳遞給ota_from_target_files
工具來實現。此標誌在 OTA 更新期間禁用設備上的真實性計算。它減少了 OTA 安裝時間,但增加了 OTA 包大小。如果您的設備未啟用 dm-verity,則傳遞此標誌無效。
一致的構建工具
問題:生成安裝文件的工具必須是一致的(給定的輸入應該總是產生相同的輸出)。
解決方案/示例:需要在以下構建工具中進行更改:
- 通知文件創建者。 NOTICE 文件創建者已更改為創建可重現的 NOTICE 集合。參考CL: https ://android.googlesource.com/platform/build/+/8ae4984c2c8009e7a08e2a76b1762c2837ad4f64。
- Java Android 編譯器工具包 (Jack) 。 Jack 工具鏈需要更新以處理生成的構造函數順序中的偶爾更改。構造函數的確定性訪問器已添加到工具鏈中: https ://android.googlesource.com/toolchain/jack/+/056a5425b3ef57935206c19ecb198a89221ca64b。
- ART AOT 編譯器 (dex2oat) 。 ART 編譯器二進製文件收到了一個更新,其中添加了一個創建確定性圖像的選項: https ://android.googlesource.com/platform/art/+/ace0dc1dd5480ad458e622085e51583653853fb9。
- libpac.so 文件 (V8) 。每個構建都會創建一個不同的
/system/lib/libpac.so
文件,因為每個構建的 V8 快照都會更改。解決方案是刪除快照: https ://android.googlesource.com/platform/external/v8/+/e537f38c36600fd0f3026adba6b3f4cbcee1fb29。 - 應用程序 pre-dexopt'd (.odex) 文件。 dexopt 之前的 (.odex) 文件在 64 位系統上包含未初始化的填充。已更正: https ://android.googlesource.com/platform/art/+/34ed3afc41820c72a3c0ab9770be66b6668aa029。
使用構建差異工具
對於無法消除與構建相關的文件更改的情況,AOSP 包含一個構建差異工具target_files_diff.py
,用於比較兩個文件包。此工具在兩個構建之間執行遞歸差異,不包括常見的構建相關文件更改,例如
- 構建輸出中的預期更改(例如,由於內部版本號更改)。
- 由於當前構建系統中的已知問題而導致的更改。
要使用構建差異工具,請運行以下命令:
target_files_diff.py dir1 dir2
dir1
和dir2
是包含每個構建的提取目標文件的基本目錄。
保持塊分配一致
對於給定的文件,雖然它的內容在兩次構建之間保持不變,但保存數據的實際塊可能已經改變。因此,更新程序必須執行不必要的 I/O 來移動塊以進行 OTA 更新。
在 Virtual A/B OTA 更新中,不必要的 I/O 會大大增加存儲寫時復制快照所需的存儲空間。在非 A/B OTA 更新中,移動塊以進行 OTA 更新會增加更新時間,因為塊移動會產生更多的 I/O。
為了解決這個問題,Google 在 Android 7.0 中擴展了make_ext4fs
工具,以保持跨構建的塊分配一致。 make_ext4fs
工具接受一個可選的-d base_fs
標誌,該標誌在生成ext4
映像時嘗試將文件分配給相同的塊。您可以從先前構建的目標文件的 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 所需的存儲空間量。
避免更新應用程序
除了最小化構建差異之外,您還可以通過排除通過應用商店獲取更新的應用的更新來減少 OTA 更新大小。 APK 通常是設備上各種分區的重要組成部分。在 OTA 更新中包含由應用商店更新的最新版本的應用可能對 OTA 包的大小影響很大,並且幾乎沒有給用戶帶來好處。當用戶收到 OTA 包時,他們可能已經直接從應用商店收到了更新的應用程序,或者甚至更新的版本。