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 には何万ものアプリがあり、そのうちの数百は共有ライブラリを使用しています。パートナーも、独自のライブラリとアプリを持っているため、影響を受けることになります。

変更による破損

dexpreopt ビルドルールを生成するには、ビルドシステムが <uses-library> 依存関係を認識している必要があります。しかし、ビルドシステムがビルドルールを生成する際には、任意のファイルの読み取りが許可されていないため、(パフォーマンスのために)マニフェストに直接アクセスして <uses-library> タグを読み取ることはできません。さらに、マニフェストが APK 内にパッケージ化されている場合や、事前にビルド済みの場合があります。そのため、<uses-library> 情報がビルドファイル(Android.bp または Android.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.bp または Android.mk に数行の追加が必要になります。

  3. 問題のあるケースでビルド時チェックと dexpreopt をモジュールごとに無効にします。dexpreopt を無効にすると、起動時に拒否されるアーティファクトのビルド時間とストレージが無駄になりません。

  4. ステップ 1 で設定した PRODUCT_BROKEN_VERIFY_USES_LIBRARIES の設定を解除して、ビルド時のチェックをグローバルに再度有効にします。この変更後、ビルドはエラーになりません(ステップ 2 と 3 のため)。

  5. ステップ 3 で無効にしたモジュールを 1 つずつ修正し、dexpreopt と <uses-library> チェックを再度有効にします。必要に応じてバグを報告します。

ビルド時の <uses-library> チェックは Android 12 で適用されます。

破損を修正する

以下のセクションでは、特定の種類の破損を修正する方法を説明します。

ビルドエラー: 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 を設定します。ビルド時の整合性チェックは引き続き実行されますが、チェックが失敗してもビルドの失敗とはなりません。チェックに失敗すると、ビルドシステムは、dexpreoopt で dex2oat コンパイラ フィルタを verify にダウングレードし、このモジュールに対する AOT コンパイルを完全に無効にします。
  • グローバルなコマンドラインによる簡単な修正を行うには、環境変数 RELAX_USES_LIBRARY_CHECK=true を使用します。これは PRODUCT_BROKEN_VERIFY_USES_LIBRARIES と同じ効果がありますが、コマンドラインで使用することが意図されています。この環境変数は、プロダクト変数をオーバーライドします。
  • エラーの根本原因の修正を行うには、ビルドシステムがマニフェスト内の <uses-library> タグを認識できるようにします。エラー メッセージを調査すると問題の原因となっているライブラリがわかります(AndroidManifest.xml や、「aapt dump badging $APK | grep uses-library」で確認できる APK 内のマニフェストを調査します)。

Android.bp モジュールの場合:

  1. モジュールの libs プロパティで欠落しているライブラリを探します。そのようなライブラリが存在する場合、通常、以下の特別な場合を除き、Soong がそのライブラリを自動的に追加します。

    • SDK ライブラリではない場合(java_sdk_library ではなく java_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 へのパスを見つけられない場合(ホスト上のビルド時のパスか、デバイス上のインストール パス)、通常はビルドが失敗します。パスが見つからない場合は、ライブラリが想定外の方法で構成されている可能性があります。問題のあるモジュールの dexpreopt を無効にして、ビルドを一時的に修正してください。

Android.bp(モジュール プロパティ):

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

Android.mk(モジュール変数):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

サポート対象外のシナリオを調査する場合は、バグを報告します。

ビルドエラー: ライブラリの依存関係がない

モジュール Y のマニフェストから Y のビルドファイルに <uses-library> X を追加しようとすると、依存関係 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 の不一致

初回起動時に、以下のように CLC の不一致に関連するメッセージがないか logcat 内を検索します。

$ 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 やカスタムクラス ローダーではなく、ライブラリのみを対象とします)。CLC は、ライブラリまたはアプリのすべての <uses-library> 依存関係の推移閉包を表すライブラリのツリーです。CLC のトップレベルの要素は、マニフェスト(クラスパス)で指定された直接の <uses-library> 依存関係です。CLC ツリーの各ノードは、独自の <uses-library> サブノードを持つ <uses-library> ノードです。

<uses-library> 依存関係は有向非巡回グラフであり、必ずしもツリーにはならないため、CLC に同じライブラリの複数のサブツリーを含めることができます。つまり、CLC はツリーに「展開された」依存関係グラフだと言えます。重複は論理レベルでのみ発生します。基盤となる実際のクラスローダーは重複しません(実行時にはライブラリごとに 1 つのクラスローダー インスタンスがあります)。

CLC は、ライブラリまたはアプリで使用される Java クラスを解決する際にライブラリをルックアップする順序を定義します。ライブラリには重複するクラスが含まれている可能性があり、クラスは最初の一致したものに解決されるため、ルックアップの順序は重要です。

オンデバイス(実行時)CLC

PackageManagerframeworks/base 内)が、Java モジュールをデバイスにロードする CLC を作成します。<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 を作成できるようにする 2 つの情報源(マニフェストの <uses-library> タグと XML 設定の共有ライブラリの依存関係)があるということです。

オンホスト(ビルド時)CLC

CLC は、ライブラリまたはアプリを読み込むときにだけでなく、コンパイル時にも必要です。コンパイルは、デバイス上で行われる場合(dexopt)と、ビルド中に行われる場合(dexpreopt)があります。dexopt はデバイス上で行われるため、PackageManager と同じ情報(マニフェストと共有ライブラリの依存関係)を持ちます。dexpreopt は、ホスト上というまったく異なる環境で行われ、ビルドシステムから同じ情報を取得する必要があります。

したがって、dexpreopt で使用されるビルド時 CLC と PackageManager で使用される実行時 CLC は同じものですが、2 つの異なる方法で算出されます。

ビルド時と実行時の CLC は一致する必要があります。そうでなければ、dexpreopt によって作成された AOT コンパイル済みのコードが拒否されます。ビルド時と実行時の CLC の等価性を確認するため、dex2oat コンパイラがビルド時の CLC を *.odex ファイルに記録します(OAT ファイル ヘッダーの classpath フィールド)。保存された CLC を確認するには、次のコマンドを使用します。

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

ビルド時と実行時の CLC の不一致は、起動中の logcat に報告されます。次のコマンドを使用して検索します。

logcat | grep -E 'ClassLoaderContext [a-z ]+ mismatch'

不一致は、ライブラリまたはアプリに dexopt または最適化なしの実行を強いるため、パフォーマンスに悪影響を及ぼします(たとえば、アプリのコードを APK からメモリ内に抽出することが必要となる場合があり、これは非常に高コストな処理です)。

共有ライブラリは、オプションにすることも必須にすることもできます。dexpreopt の観点からは、必要なライブラリがビルド時に存在している必要があります(存在しない場合はビルドエラーになります)。オプションのライブラリは、ビルド時に存在してもしなくても構いません。存在する場合は、CLC に追加され、dex2oat に渡されて、*.odex ファイルに記録されます。オプションのライブラリが存在しない場合、スキップされ、CLC には追加されません。ビルド時と実行時で状態に不一致がある場合(一方ではオプションのライブラリが存在するが、もう一方では存在しない)、ビルド時と実行時の CLC は一致せず、コンパイル済みのコードは拒否されます。

高度なビルドシステムの詳細(マニフェスト修正ツール)

ライブラリまたはアプリのソース マニフェストに <uses-library> タグがない場合があります。たとえば、ライブラリまたはアプリの推移的依存関係の 1 つが別の <uses-library> タグを使用して始まり、ライブラリまたはアプリのマニフェストがそのタグが含まれるように更新されていない場合などです。

Soong は、与えられたライブラリまたはアプリの欠落している <uses-library> タグの一部を、ライブラリまたはアプリの推移的依存関係閉包内の SDK ライブラリとして自動的に算出できます。ライブラリ(またはアプリ)が、SDK ライブラリに依存する静的ライブラリに依存し、別のライブラリを通じて推移的に依存する可能性があるため、閉包が必要になります。

すべての <uses-library> タグをこの方法で算出できるわけではありませんが、可能であれば、Soong でマニフェスト エントリを自動的に追加することをおすすめします。エラーが発生しにくく、メンテナンスがシンプルになります。たとえば、多くのアプリで新しい <uses-library> 依存関係を追加する静的ライブラリを使用していると、すべての依存関係を更新する必要がありますが、このメンテナンスは困難です。