Dexpreopt 和 <uses-library>支票

Android 12 對具有<uses-library>依賴項的 Java 模組的 DEX 檔案 (dexpreopt) 的 AOT 編譯進行了建置系統變更。在某些情況下,這些建置系統變更可能會破壞建置。使用此頁面為損壞做好準備,並按照此頁面上的方法修復和減輕損壞。

Dexpreopt 是 Java 函式庫和應用程式的提前編譯過程。 Deexpreopt 在建置時發生在主機上(與dexopt不同,後者發生在裝置上)。 Java 模組(庫或應用程式)使用的共享庫相依性的結構稱為其類別載入器上下文(CLC)。為了保證 dexpreopt 的正確性,建置時和執行時 CLC 必須一致。建置時 CLC 是 dex2oat 編譯器在 dexpreopt 時所使用的內容(記錄在 ODEX 檔案中),執行階段 CLC 是預編譯程式碼載入到裝置上的上下文。

出於正確性和效能的原因,這些建置時和運行時 CLC 必須一致。為了正確性,有必要處理重複的類別。如果執行階段的共用程式庫相依性與編譯時使用的共用程式庫相依性不同,則某些類別可能會以不同的方式解析,從而導致微妙的執行時間錯誤。效能也會受到重複類別的運行時檢查的影響。

受影響的用例

第一次啟動是受這些變更影響的主要用例:如果 ART 偵測到建置時和執行時間 CLC 之間不匹配,它會拒絕 dexpreopt 工件並執行 dexopt。對於後續啟動來說,這很好,因為應用程式可以在背景進行 dexopted 並儲存在磁碟上。

Android 受影響的區域

這會影響所有在運行時依賴其他 Java 程式庫的 Java 應用程式和程式庫。 Android 擁有數千個應用程序,其中數百個應用程式使用共用程式庫。合作夥伴也會受到影響,因為他們有自己的程式庫和應用程式。

中斷變更

建置系統在產生 dexpreopt 建置規則之前需要了解<uses-library>依賴項。但是,它無法直接存取清單並讀取其中的<uses-library>標記,因為建置系統在生成建置規則時不允許讀取任意檔案(出於效能原因)。此外,清單可能會打包在 APK 或預先建置的內部。因此, <uses-library>資訊必須存在於建置檔案( Android.bpAndroid.mk )中。

先前,ART 使用了一種忽略共享庫相依性(稱為&-classpath )的解決方法。這是不安全的,並且會導致細微的錯誤,因此該解決方法在 Android 12 中被刪除。

因此,在其建置檔中未提供正確的<uses-library>資訊的 Java 模組可能會導致建置破壞(由建置時 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. 如果上一個步驟未提供解決方案,請在 Android 上新增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 的路徑(主機上的建置時路徑或裝置上的安裝路徑),通常會導致建置失敗。找不到路徑可能表明該庫是以某種意外的方式配置的。透過禁用有問題的模組的 dexpreopt 來臨時修復建置。

Android.bp(模組屬性):

enforce_uses_libs: false,
dex_preopt: {
    enabled: false,
},

Android.mk(模組變數):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

提交錯誤以調查任何不受支援的場景。

建置錯誤:缺少庫依賴項

嘗試將<uses-library> X 從模組 Y 的清單新增至 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 和執行時間 CLC 的相等性,dex2oat 編譯器將建置時 CLC 記錄在*.odex檔案中(在 OAT 檔案頭的classpath欄位中)。若要尋找儲存的 CLC,請使用下列命令:

oatdump --oat-file=<FILE> | grep '^classpath = '

啟動期間 logcat 中會報告建置時和執行時間 CLC 不符。使用以下命令搜尋它:

logcat | grep -E 'ClassLoaderContext [az ]+ mismatch'

不匹配對效能不利,因為它迫使庫或應用程式要么被刪除,要么在沒有優化的情況下運行(例如,應用程式的程式碼可能需要從APK 中提取到記憶體中,這是一個非常昂貴的操作) 。

共享庫可以是可選的,也可以是必需的。從 dexpreopt 的角度來看,建置時必須存在所需的函式庫(缺少它是建置錯誤)。可選庫在建置時可以存在或不存在:如果存在,則會將其新增至 CLC、傳遞至 dex2oat 並記錄在*.odex檔案中。如果可選庫不存在,則會跳過該庫並且不會將其新增至 CLC 。如果建置時和運行時狀態不匹配(在一種情況下存在可選庫,但在另一種情況下不存在),則建置時和運行時 CLC 不匹配,編譯後的程式碼將被拒絕。

高級建置系統詳細資訊(清單修復程式​​)

有時,庫或應用程式的來源清單中缺少<uses-library>標籤。例如,如果程式庫或應用程式的傳遞依賴項之一開始使用另一個<uses-library>標記,且程式庫或應用程式的清單未更新以包含它,則可能會發生這種情況。

Soong 可以自動計算給定庫或應用程式缺少的一些<uses-library>標籤,作為庫或應用程式的傳遞依賴閉包中的 SDK 庫。需要閉包,因為庫(或應用程式)可能依賴依賴 SDK 庫的靜態庫,並且可能再次透過另一個庫傳遞依賴。

並非所有<uses-library>標籤都可以透過這種方式計算,但如果可能,最好讓 Soong 自動新增清單條目;它不易出錯並簡化了維護。例如,當許多應用程式使用新增新的<uses-library>依賴項的靜態函式庫時,所有應用程式都必須更新,這很難維護。