Dexpreopt 和 <uses-library> 檢查

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.bpAndroid.mk) 中必須有 <uses-library> 資訊。

先前 ART 使用的解決方法會忽略共用程式庫依附元件 (稱為 &-classpath)。這種做法不安全,且會導致細微的錯誤,因此 Android 12 已移除這項解決方法。

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

遷移路徑

請按照下列步驟修正損毀的建構版本:

  1. 透過設定,為特定產品全域停用建構時間檢查

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    產品 makefile 中。這項作業可修正建構錯誤 (「修正中斷問題」一節列出的特殊情況除外)。不過,這只是暫時的解決方法,可能會導致開機時 CLC 不符,接著進行 dexopt。

  2. 在全域停用建構時間檢查之前,請先修正失敗的模組,方法是在建構檔案中加入必要的 <uses-library> 資訊 (詳情請參閱「修正中斷問題」)。對於大多數模組,這需要在 Android.bpAndroid.mk 中新增幾行。

  3. 針對有問題的案例,逐一停用模組的建構時間檢查和 dexpreopt。停用 dexpreopt,以免在啟動時遭拒的構件上浪費建構時間和儲存空間。

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

  5. 逐一修正您在步驟 3 中停用的模組,然後重新啟用 dexpreopt 和 <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. 在模組的 Android.mk 定義中,新增必要程式庫的 LOCAL_USES_LIBRARIES := <library-module-name>,以及選用程式庫的 LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name>。這些屬性接受模組名稱清單。清單中程式庫的相對順序必須與資訊清單中的順序相同。

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

如果建構系統找不到 <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 的頂層元素是資訊清單 (類別路徑) 中指定的直接 <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 不僅在載入程式庫或應用程式時需要,編譯時也需要。編譯作業可以在裝置上 (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> 依附元件的靜態程式庫,就必須更新所有應用程式,這會造成維護上的困難。