Android 12 的建構系統已針對具有 <uses-library>
依附元件的 Java 模組,對 DEX 檔案的 AOT 編譯 (dexpreopt) 進行變更。在某些情況下,這些建構系統變更可能會導致建構作業中斷。請參閱本頁面,瞭解如何因應中斷情形,並按照本頁面提供的解決方法修正及減輕中斷情形。
Dexpreopt 是 Java 程式庫和應用程式的提前編譯程序。Dexpreopt 會在建構期間於主機上執行 (與在裝置上執行的 dexopt 不同)。Java 模組 (程式庫或應用程式) 使用的共用程式庫依附元件結構稱為其類別載入器內容 (CLC)。為確保 dexpreopt 的正確性,建構時間和執行時間的 CLC 必須一致。建構時間 CLC 是 dex2oat 編譯器在 dexpreopt 時間點使用的內容 (會記錄在 ODEX 檔案中),而執行時間 CLC 則是裝置上載入預先編譯程式碼的內容。
基於正確性和效能考量,這些建構時間和執行時間 CLC 必須一致。為了確保正確性,您必須處理重複的類別。如果執行階段的共用程式庫依附元件與編譯時使用的依附元件不同,部分類別可能會以不同的方式解析,導致細微的執行階段錯誤。效能也會受到檢查重複類別的執行階段影響。
受影響的用途
首次啟動是受這些變更影響的主要用途:如果 ART 偵測到建構時間和執行時間 CLC 不相符,就會拒絕 dexpreopt 構件,改為執行 dexopt。對於後續的啟動作業來說,這並無妨礙,因為應用程式可以在背景中進行 dexopt,並儲存在磁碟上。
Android 受影響的部分
這會影響所有 Java 應用程式和程式庫,因為這些應用程式和程式庫會對其他 Java 程式庫產生執行階段依附元件。Android 有成千上百個應用程式,其中有數百個使用共用程式庫。合作夥伴也會受到影響,因為他們有自己的程式庫和應用程式。
破壞性變更
建構系統必須先瞭解 <uses-library>
依附元件,才能產生 dexpreopt 建構規則。不過,由於產生建構規則時 (基於效能考量),建構系統不得讀取任意檔案,因此無法直接存取資訊清單並讀取其中的 <uses-library>
標記。此外,資訊清單可能會封裝在 APK 或預先建構的內容中。因此,建構檔案 (Android.bp
或 Android.mk
) 中必須包含 <uses-library>
資訊。
先前 ART 使用了一個解決方法,可忽略共用程式庫依附元件 (稱為 &-classpath
)。這個方法不安全,且會導致細微的錯誤,因此在 Android 12 中已移除。
因此,如果 Java 模組未在建構檔案中提供正確的 <uses-library>
資訊,就可能導致建構作業中斷 (建構時間 CLC 不相符) 或首次啟動時間回歸 (啟動時間 CLC 不相符,接著進行 dexopt)。
遷移路徑
請按照下列步驟修正損毀的版本:
透過設定,針對特定產品在全球停用建構時檢查
PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true
在產品 makefile 中。這麼做可修正建構錯誤 (除了「修正中斷情形」一節所列的特殊情況)。不過,這是臨時性替代方案,可能會導致啟動時 CLC 不相符,接著會進行 dexopt。
在全面停用建構時間檢查之前,請先修正失敗的模組,方法是將必要的
<uses-library>
資訊新增至建構檔案 (詳情請參閱「修正損壞問題」)。對於大多數模組,您需要在Android.bp
或Android.mk
中新增幾行程式碼。針對有問題的情況,針對個別模組停用建構時間檢查和 dexpreopt。停用 dexpreopt,避免在啟動時遭到拒絕的構件浪費建構時間和儲存空間。
透過取消設定步驟 1 中設定的
PRODUCT_BROKEN_VERIFY_USES_LIBRARIES
,全域重新啟用建構時檢查功能;在進行這項變更後,建構作業不應失敗 (因為步驟 2 和 3 已完成)。逐一修正您在步驟 3 中停用的模組,然後重新啟用 dexpreopt 和
<uses-library>
檢查。視需要回報錯誤。
Android 12 會強制執行建構時 <uses-library>
檢查。
修正中斷問題
以下各節說明如何修正特定類型的中斷情形。
建構錯誤:CLC 不相符
建構系統會在 Android.bp
或 Android.mk
檔案與資訊清單之間執行建構時一致性檢查。建構系統無法讀取資訊清單,但可以產生建構規則來讀取資訊清單 (必要時從 APK 中擷取資訊清單),並比較資訊清單中的 <uses-library>
標記與建構檔案中的 <uses-library>
資訊。如果檢查失敗,錯誤會如下所示:
error: mismatch in the <uses-library> tags between the build system and the manifest:
- required libraries in build system: []
vs. in the manifest: [org.apache.http.legacy]
- optional libraries in build system: []
vs. in the manifest: [com.x.y.z]
- tags in the manifest (.../X_intermediates/manifest/AndroidManifest.xml):
<uses-library android:name="com.x.y.z"/>
<uses-library android:name="org.apache.http.legacy"/>
note: the following options are available:
- to temporarily disable the check on command line, rebuild with RELAX_USES_LIBRARY_CHECK=true (this will set compiler filter "verify" and disable AOT-compilation in dexpreopt)
- to temporarily disable the check for the whole product, set PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true in the product makefiles
- to fix the check, make build system properties coherent with the manifest
- see build/make/Changes.md for details
如錯誤訊息所示,視緊急程度而定,有幾種解決方法:
- 如需產品層級的暫時性修正,請在產品 makefile 中設定
PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true
。系統仍會執行建構時的一致性檢查,但檢查失敗不代表建構失敗。相反地,檢查失敗會導致建構系統將 dex2oat 編譯器篩選器降級為 dexpreopt 中的verify
,這會完全停用此模組的 AOT 編譯。 - 如要快速修正全域指令列問題,請使用環境變數
RELAX_USES_LIBRARY_CHECK=true
。這與PRODUCT_BROKEN_VERIFY_USES_LIBRARIES
的效果相同,但可用於指令列。環境變數會覆寫產品變數。 - 如要徹底解決錯誤,請讓建構系統瞭解資訊清單中的
<uses-library>
標記。檢查錯誤訊息,即可瞭解哪些程式庫導致問題 (檢查AndroidManifest.xml
或 APK 內的資訊清單也是如此,可使用 `aapt dump badging $APK | grep uses-library
` 進行檢查)。
Android.bp
模組:
在模組的
libs
屬性中尋找缺少的程式庫。如果有,Soong 通常會自動新增此類程式庫,但以下特殊情況除外:- 程式庫不是 SDK 程式庫 (定義為
java_library
,而非java_sdk_library
)。 - 程式庫的程式庫名稱 (在資訊清單中) 與模組名稱 (在建構系統中) 不同。
如要暫時修正這個問題,請在
Android.bp
程式庫定義中新增provides_uses_lib: "<library-name>"
。如要長期解決問題,請修正基礎問題:將程式庫轉換為 SDK 程式庫,或重新命名其模組。- 程式庫不是 SDK 程式庫 (定義為
如果上一個步驟未提供解決方案,請在模組的
Android.bp
定義中,為必要程式庫新增uses_libs: ["<library-module-name>"]
,或為選用程式庫新增optional_uses_libs: ["<library-module-name>"]
。這些屬性會接受模組名稱清單。清單中程式庫的相對順序必須與資訊清單中的順序相同。
Android.mk
模組:
請檢查程式庫的程式庫名稱 (在資訊清單中) 是否與模組名稱 (在建構系統中) 不同。如果是這樣,請在程式庫的
Android.mk
檔案中新增LOCAL_PROVIDES_USES_LIBRARY := <library-name>
,或在程式庫的Android.bp
檔案中新增provides_uses_lib: "<library-name>"
,以便暫時修正這個問題 (由於Android.mk
模組可能會依附Android.bp
程式庫,因此兩種情況皆有可能)。如要長期解決問題,請修正基礎問題:重新命名程式庫模組。新增
LOCAL_USES_LIBRARIES := <library-module-name>
以用於必要程式庫;新增LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name>
以用於模組的Android.mk
定義,用於選用程式庫。這些屬性接受模組名稱清單。清單中程式庫的相對順序必須與資訊清單中的順序相同。
建構錯誤:不明的程式庫路徑
如果建構系統找不到 <uses-library>
DEX jar 的路徑 (主機上的建構時間路徑或裝置上的安裝路徑),通常會導致建構失敗。如果找不到路徑,可能表示程式庫是以某種非預期的方式進行設定。針對有問題的模組,停用 dexpreopt 來暫時修正建構作業。
Android.bp (模組屬性):
enforce_uses_libs: false,
dex_preopt: {
enabled: false,
},
Android.mk (模組變數):
LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false
回報錯誤,以便調查任何不支援的情況。
建構錯誤:缺少程式庫依附元件
嘗試將模組 Y 的資訊清單中的 <uses-library>
X 新增至 Y 的建構檔案,可能會因缺少依附元件 X 而導致建構錯誤。
以下是 Android.bp 模組的錯誤訊息範例:
"Y" depends on undefined module "X"
以下是 Android.mk 模組的錯誤訊息範例:
'.../JAVA_LIBRARIES/com.android.X_intermediates/dexpreopt.config', needed by '.../APPS/Y_intermediates/enforce_uses_libraries.status', missing and no known rule to make it
這類錯誤的常見原因是程式庫的名稱與建構系統中對應的模組名稱不同。舉例來說,如果資訊清單 <uses-library>
項目為 com.android.X
,但程式庫模組的名稱只是 X
,就會導致錯誤。如要解決這個問題,請告訴建構系統,名為 X
的模組提供名為 com.android.X
的 <uses-library>
。
以下是 Android.bp
程式庫 (模組屬性) 的範例:
provides_uses_lib: “com.android.X”,
以下是 Android.mk 程式庫 (模組變數) 的範例:
LOCAL_PROVIDES_USES_LIBRARY := com.android.X
開機時 CLC 不相符
在第一次啟動時,請在 logcat 中搜尋與 CLC 不相符相關的訊息,如下所示:
$ adb wait-for-device && adb logcat \
| grep -E 'ClassLoaderContext [a-z ]+ mismatch' -A1
輸出結果可能會顯示以下格式的訊息:
[...] W system_server: ClassLoaderContext shared library size mismatch Expected=..., found=... (PCL[]... | PCL[]...)
[...] I PackageDexOptimizer: Running dexopt (dexoptNeeded=1) on: ...
如果您收到 CLC 不相符警告,請尋找錯誤模組的 dexopt 指令。如要修正這個問題,請確認模組的建構時間檢查通過。如果這樣做無法解決問題,表示您的情況可能是建構系統不支援的特殊情況 (例如應用程式會載入其他 APK,而非程式庫)。建構系統無法處理所有情況,因為在建構期間,無法確定應用程式在執行階段載入的內容。
類別載入器背景
CLC 是描述類別載入器階層的樹狀結構。建構系統會以狹義方式使用 CLC (僅涵蓋程式庫,不涵蓋 APK 或自訂類別載入器):這是一個程式庫樹狀結構,代表程式庫或應用程式的所有 <uses-library>
依附元件的間接閉包。CLC 的頂層元素是資訊清單 (classpath) 中指定的直接 <uses-library>
依附元件。CLC 樹狀結構的每個節點都是 <uses-library>
節點,可能會有自己的 <uses-library>
子節點。
由於 <uses-library>
依附元件是導向無環圖,不一定是樹狀結構,因此 CLC 可包含相同程式庫的多個子樹狀結構。換句話說,CLC 是依附元件圖表「展開」為樹狀結構。重複的只是邏輯層級,實際的基礎類別載入器並未重複 (在執行階段,每個程式庫只有一個類別載入器例項)。
CLC 會在解析程式庫或應用程式所使用的 Java 類別時,定義程式庫的查詢順序。查詢順序很重要,因為程式庫可能包含重複的類別,而系統會將類別解析為第一個相符項目。
裝置端 (執行階段) CLC
PackageManager
(在 frameworks/base
中) 會建立 CLC,以便在裝置上載入 Java 模組。它會將模組資訊清單中 <uses-library>
標記中列出的程式庫新增為頂層 CLC 元素。
對於每個使用的程式庫,PackageManager
會取得所有 <uses-library>
依附元件 (在該程式庫的資訊清單中指定為標記),並為每個依附元件新增巢狀 CLC。這個程序會持續遞迴,直到建構的 CLC 樹狀結構的所有葉節點都是沒有 <uses-library>
依附元件的程式庫為止。
PackageManager
只會辨識共用程式庫。這裡的「共用」定義與一般意義不同 (例如共用與靜態)。在 Android 中,Java 共用程式庫是指裝置上安裝的 XML 設定檔 (/system/etc/permissions/platform.xml
) 中列出的共用程式庫。每個項目都包含共用程式庫的名稱、其 DEX JAR 檔案的路徑,以及依附元件清單 (這個共用程式庫在執行階段使用的其他共用程式庫,並在其資訊清單中以 <uses-library>
標記指定)。
換句話說,有兩個資訊來源可讓 PackageManager
在執行階段建構 CLC:資訊清單中的 <uses-library>
標記,以及 XML 設定檔中的共用程式庫依附元件。
主機 (建構時間) CLC
除了載入程式庫或應用程式時需要 CLC,編譯時也需要 CLC。編譯作業可在裝置上 (dexopt) 或建構期間 (dexpreopt) 進行。由於 dexopt 是在裝置上執行,因此與 PackageManager
相同 (資訊清單和共用程式庫依附元件)。不過,Dexpreopt 會在主機上執行,且處於完全不同的環境,因此必須從建構系統取得相同的資訊。
因此,deXpreopt 使用的建構時間 CLC 和 PackageManager
使用的執行時間 CLC 是相同的,但計算方式不同。
建構時間和執行時間 CLC 必須一致,否則由 dexpreopt 建立的 AOT 編譯程式碼會遭到拒絕。為檢查建構時間和執行時間 CLC 是否相等,dex2oat 編譯器會在 *.odex
檔案中記錄建構時間 CLC (在 OAT 檔案標頭的 classpath
欄位中)。如要找出儲存的 CLC,請使用以下指令:
oatdump --oat-file=<FILE> | grep '^classpath = '
在啟動期間,logcat 會回報建構時間和執行時間 CLC 不相符。使用以下指令搜尋:
logcat | grep -E 'ClassLoaderContext [a-z ]+ mismatch'
不相符會影響效能,因為這會迫使程式庫或應用程式進行 dexopt,或在沒有最佳化情況下執行 (例如,應用程式的程式碼可能需要從 APK 中擷取記憶體,這是非常耗費資源的作業)。
共用程式庫可以是選用或必要的。從 dexpreopt 的角度來看,必須在建構期間提供必要的程式庫 (否則會發生建構錯誤)。在建構期間,可選的程式庫可以存在或不存在:如果存在,則會新增至 CLC,傳遞至 dex2oat,並記錄在 *.odex
檔案中。如果沒有選用程式庫,系統會略過該程式庫,且不會新增至 CLC。如果建構時間和執行時間狀態不相符 (在某種情況下有提供選用程式庫,但在另一種情況下則沒有),則建構時間和執行時間 CLC 不相符,系統會拒絕已編譯的程式碼。
進階建構系統詳細資料 (資訊清單修正工具)
有時程式庫或應用程式的來源資訊清單中會缺少 <uses-library>
標記。例如,如果程式庫或應用程式其中一個傳遞依附元件開始使用另一個 <uses-library>
標記,而程式庫或應用程式的資訊清單未更新以納入該標記,就可能發生這種情況。
Soong 可自動計算特定程式庫或應用程式缺少的部分 <uses-library>
標記,這些標記是程式庫或應用程式間接依附元件關閉時的 SDK 程式庫。關閉是必要的,因為程式庫 (或應用程式) 可能會依附於依附 SDK 程式庫的靜態資料庫,並可能會透過另一個程式庫間接依附。
並非所有 <uses-library>
標記都能以這種方式計算,但在可行情況下,建議讓 Soong 自動新增資訊清單項目,這樣不易發生錯誤,也能簡化維護作業。舉例來說,如果許多應用程式都使用新增 <uses-library>
依附元件的靜態程式庫,就必須更新所有應用程式,這會造成維護上的困難。