Dexpreopt 和 <uses-library> 檢查

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

Dexpreopt 是 Java 庫和應用程序的提前編譯過程。 Dexpreopt 在構建時發生在主機上(與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

    在產品生成文件中。這修復了構建錯誤(除了特殊情況,在修復破損部分中列出)。但是,這是一種臨時解決方法,它可能會導致引導時 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 <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>"]添加到 Android 中,或者將可選庫optional_uses_libs: ["<library-module-name>"]添加到Android.bp定義模塊。這些屬性接受模塊名稱列表。列表中庫的相對順序必須與清單中的順序相同。

對於Android.mk模塊:

  1. 檢查庫是否具有與其模塊名稱(在構建系統中)不同的庫名稱(在清單中)。如果是這樣,請通過在庫的 Android.mk 文件中添加LOCAL_PROVIDES_USES_LIBRARY := <library-name>來臨時修復此問題,或者在庫的Android.mk文件中添加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

提交錯誤以調查任何不受支持的方案。

構建錯誤:缺少庫依賴項

嘗試將模塊 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 編譯器將構建時 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>依賴的靜態庫時,所有應用都必須更新,這很難維護。