Dexpreopt 和 <uses-library>檢查

Android 12 的建構系統已針對具有 <uses-library> 依附元件的 Java 模組,對 DEX 檔案的 AOT 編譯 (dexpreopt) 進行變更。在某些情況下,這些建構系統變更可能會導致建構中斷。請參閱本頁面,瞭解如何因應中斷情形,並按照本頁面提供的解決方法修正及減輕中斷情形。

Dexpreopt 是 Java 程式庫和應用程式的提前編譯程序。Dexpreopt 在主機建構期間發生 (而不是在裝置端執行的 dexopt)。Java 模組 (程式庫或應用程式) 使用的共用程式庫依附元件結構,稱為「類別載入器結構定義」 (CLC)。為了確保重建的正確性,建構時間和執行階段 CLC 必須一致。建構時間 CLC 是 dex2oat 編譯器在 dexpreopt 時間點使用的內容 (會記錄在 ODEX 檔案中),而執行時間 CLC 則是裝置上載入預先編譯程式碼的內容。

基於正確性和效能的考量,這些建構時間和執行階段 CLC 必須一致。為了確保正確性,您必須處理重複的類別。如果執行階段的共用程式庫依附元件與編譯時使用的程式庫依附元件不同,部分類別可能會有不同的解析方式,導致出現細微的執行階段錯誤。執行階段檢查重複類別時也會對效能造成影響。

受影響的用途

第一次啟動是受這些變更影響的主要用途:如果 ART 偵測到建構時間與執行階段 CLC 不符,就會拒絕淘汰成果並改為執行 dexopt。應用程式可在背景進行 dex 處理,並儲存在磁碟上,因此在後續啟動作業沒有問題。

Android 受影響的區域

這會影響所有 Java 應用程式和程式庫,這些應用程式和程式庫在執行階段會依附其他 Java 程式庫。Android 提供數千款應用程式,其中數百個應用程式都使用共用資料庫。合作夥伴也會受到影響,因為他們有自己的程式庫和應用程式。

破壞性變更

建構系統必須先瞭解 <uses-library> 依附元件,才能產生 dreopt 建構規則。不過,由於產生建構規則時 (基於效能考量),建構系統不得讀取任意檔案,因此無法直接存取資訊清單並讀取其中的 <uses-library> 標記。此外,資訊清單可能會封裝在 APK 或預先建構的內容中。因此,建構檔案 (Android.bpAndroid.mk) 中必須包含 <uses-library> 資訊。

先前 ART 使用略過共用程式庫依附元件的解決方法 (稱為 &-classpath)。這不安全,並造成細微的錯誤,因此這個解決方法已在 Android 12 中移除。

因此,如果 Java 模組未在建構檔案中提供正確的 <uses-library> 資訊,就可能導致建構作業中斷 (建構時間 CLC 不相符) 或首次啟動時間回歸 (啟動時間 CLC 不相符,接著進行 dexopt)。

遷移路徑

請按照下列步驟修正故障的版本:

  1. 透過設定,針對特定產品在全球停用建構時檢查

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    。這修正了建構錯誤 (「修正中斷」一節中列出的特殊情況除外)。不過,這只是暫時性的解決方法,而且可能會導致開機時間 CLC 不符,之後則執行 dexopt。

  2. 如果模組在全域停用建構時間檢查前失敗,您可以在其建構檔案中加入必要的 <uses-library> 資訊,以修正這些模組失敗的問題 (詳情請參閱「修正中斷」一節)。對於大多數模組,您需要在 Android.bpAndroid.mk 中新增幾行程式碼。

  3. 針對有問題的情況,針對個別模組停用建構時間檢查和 dexpreopt。停用 dexpreopt 後,就不會浪費在開機時遭到拒絕的構件建構時間和儲存空間。

  4. 透過取消設定步驟 1 中設定的 PRODUCT_BROKEN_VERIFY_USES_LIBRARIES,全域重新啟用建構時檢查功能;在進行這項變更後,建構作業不應失敗 (因為步驟 2 和 3 已完成)。

  5. 修正您在步驟 3 中停用的模組 (一次一個),然後再重新啟用重新啟用和 <uses-library> 檢查。視需要回報錯誤。

系統會在 Android 12 中強制執行建構時間 <uses-library> 檢查。

修正服務中斷情形

以下各節說明如何修正特定類型的中斷情形。

建構錯誤:CLC 不相符

建構系統會在 Android.bpAndroid.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 模組:

  1. 在模組的 libs 屬性中尋找缺少的程式庫。在此情況下,Soong 通常會自動新增這類程式庫,除非遇到以下特殊情況:

    • 程式庫不是 SDK 程式庫 (定義為 java_library,而非 java_sdk_library)。
    • 程式庫的程式庫名稱 (在資訊清單中) 與模組名稱 (在建構系統中) 不同。

    如要暫時修正這個問題,請在 Android.bp 程式庫定義中加入 provides_uses_lib: "<library-name>"。如要長期解決問題,請修正基礎問題:將程式庫轉換為 SDK 程式庫,或重新命名其模組。

  2. 如果上一個步驟未提供解析度,請為必要程式庫加入 uses_libs: ["<library-module-name>"],或者將選用程式庫的 optional_uses_libs: ["<library-module-name>"] 新增至模組的 Android.bp 定義。這些屬性接受模組名稱清單。清單上的程式庫相對順序必須與資訊清單中的順序相同。

Android.mk 模組:

  1. 檢查程式庫的程式庫名稱 (在資訊清單中) 是否與建構系統中的模組名稱不同。如果可以,請在程式庫的 Android.mk 檔案中加入 LOCAL_PROVIDES_USES_LIBRARY := <library-name>,或在程式庫的 Android.bp 檔案中新增 provides_uses_lib: "<library-name>",以暫時修正此問題 (這兩種情況都可能,因為 Android.mk 模組可能依附於 Android.bp 程式庫)。長期解決方案可以修正潛在問題:重新命名程式庫模組。

  2. 新增 LOCAL_USES_LIBRARIES := <library-module-name> 以用於必要程式庫;新增 LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> 以用於模組的 Android.mk 定義,用於選用程式庫。這些屬性接受模組名稱清單。清單中程式庫的相對順序必須與資訊清單中的順序相同。

建構錯誤:不明的程式庫路徑

如果建構系統找不到 <uses-library> DEX jar 的路徑 (主機上的建構時間路徑或裝置上的安裝路徑),通常會導致建構失敗。如果無法找到路徑,可能表示程式庫設定以非預期的方式設定。停用有問題的模組的重新啟用功能,暫時修正建構作業。

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 的頂層元素是資訊清單中指定的直接 <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 會在主機上執行,且處於完全不同的環境,因此必須從建構系統取得相同的資訊。

因此,Dreopt 使用的建構時間 CLC 和 PackageManager 使用的執行階段 CLC 相同,只不過是以兩種不同的方式計算。

建構時間和執行階段 CLC 必須與 Dexpreopt 建立,否則系統會拒絕由 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> 依附元件,則所有應用程式都必須更新,維護不易維護。