R8 でプラットフォームのコードとリソースを最適化する

Android プラットフォームのビルドシステム(Soong)は、Android アプリ(android_app)、テスト(android_test)、compile_dex: true(services.jar など)を含むインストール可能な Java ライブラリなど、DEX バイトコードをコンパイルするターゲットで R8 コンパイラを実行し、未使用のコードとリソースを縮小、最適化、削除します。静的ライブラリ(android_library と静的 java_library)では、Soong は R8 を直接実行しませんが、optimize プロパティ ブロックを使用して、それらを静的にリンクするダウンストリーム DEX ターゲットにコンシューマー keep ルール(export_proguard_flags_files: true)をアタッチして伝播します。

システム イメージやベンダー パッケージを構築するプラットフォーム エンジニアにとって、広すぎる保持ルールを排除することは、システムの健全性に直接的なメリットをもたらします。

  • システム パーティションのフットプリントの縮小: R8 は、APK と JAR を /system、/system_ext、/product、/vendor にパッケージ化する前に、不要なクラス、メソッド、リソースを削除します。
  • コンパイル済みアーティファクトのサイズが小さくなる: DEX メソッドの数が減るため、ビルド時またはデバイス上のコンパイル時に dex2oat によって生成される .odex ファイルと .vdex ファイルのサイズが小さくなります。
  • ランタイム メモリのプレッシャーの軽減: Android は、デマンド ページングの mmap 呼び出しを使用して .odex、.vdex、APK リソース テーブルをプロセス メモリにマッピングするため、バイナリが小さくなると、アプリの起動時のメジャー ページ フォルトが減少し、プロセス間の常駐コードとリソース メモリが削減されます。コンパイルされたアプリコードがデバイスのメモリに与える影響について詳しくは、アプリコードはメモリをご覧ください。
  • より効果的なプログラム全体の最適化: 狭い保持制約により、R8 はメソッドのインライン化、インターフェースと仮想呼び出しの非仮想化、未使用フィールドの削除、クラス間の定数の伝播を行うことができます。

Soong で R8 最適化を設定する

Android.bp ファイルで、android_app とインストール可能な java_library ターゲット(または android_library と静的 java_library モジュールでコンシューマー保持ルールをエクスポート)の optimize プロパティ ブロックを使用して R8 最適化を構成します。

android_app {
    name: "MySystemApp",
    srcs: ["src/**/*.java"],
    optimize: {
        obfuscate: true,
        shrink_resources: true,
    },
}

次の表に、Soong で最も一般的な optimize プロパティ(build/soong/java/dex.go で定義)をまとめます。

プロパティ 説明
enabled R8 をターゲットで実行するかどうかを制御します。デフォルトでは、すべての android_app ターゲットに対して true が設定されます。
shrink ツリー シェイキングを制御して、到達できないクラス、フィールド、メソッドを削除します。android_app ターゲットの場合、デフォルトは true です(スタンドアロンの java_library とテスト モジュールの場合、明示的に設定しない限り -dontshrink を渡す false です)。
optimize メソッドのインライン化、クラスの統合、定数の伝播、デッドブランチの削除などのバイトコードの最適化を制御します。デフォルトは、android_app ターゲットの場合は true(RELEASE_R8_OPTIMIZE_BY_DEFAULT リリースビルドフラグで制御)です。
obfuscate 識別子の最小化と名前の変更を制御します。過去の互換性のためにデフォルトは false ですが、可能な限り true に設定して DEX のサイズを削減し、より深い最適化を可能にします(可能な限り難読化を有効にするを参照)。
shrink_resources コード圧縮後に、パッケージ化された APK から未使用のリソース(res/ エントリ)を削除します。デフォルトは false です。
optimized_shrink_resources 統合された R8 コードとリソースのシュリンカー パイプラインを実行し、R8 がコードとリソースの参照を 1 回のパスで同時にトレースできるようにします。shrink_resources: true が設定されている場合、デフォルトは RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT(標準プラットフォーム ビルドでは true)です。
proguard_flags_files カスタム保持ルールを含むモジュール固有の .flags ファイルまたは .pro ファイルを一覧表示します。標準のアノテーションまたはデフォルトで十分な場合は、カスタム ファイルを追加しないでください。
export_proguard_flags_files このライブラリの proguard_flags_files を、静的に依存するダウンストリーム モジュールに伝播します。
trace_references_from このターゲットへのバイトコード参照を R8 が自動的にトレースして保持する Java ライブラリ コンパニオン ターゲットをリストします。

可能な限り難読化を有効にする

Soong では、歴史的な互換性のために obfuscate はデフォルトで false になっていますが、可能な限り android_app とスタンドアロン DEX ターゲットで obfuscate: true を明示的に設定する必要があります。

  • DEX と .vdex フットプリントの縮小: パッケージ、クラス、フィールド、メソッドの名前を短い識別子(a、b)に変更すると、DEX 文字列プール(string_ids と string_data_item)と型記述子が縮小されます。Android は .vdex ファイルと .odex ファイルをプロセス メモリにマッピングするため、シンボル テーブルを小さくすると、システム パーティションのサイズとランタイム メモリ使用量の両方が直接削減されます。
  • プログラム全体のより深い最適化: R8 に識別子の名前変更を許可すると、R8 が名前の衝突を避けるためにスキップしなければならないパッケージ全体で、クラスの統合、パッケージのフラット化、合成クラスの重複除去が可能になります。
  • フル スタック トレースのシンボル化: Soong で難読化を有効にしても、デバッグの容易さは低下しません。R8 でコンパイルされたターゲットごとに、Soong はモジュールの中間ディレクトリに proguard_dictionary マッピング ファイルを出力し、すべてのモジュール ディクショナリをビルドの proguard-dict.zip アーティファクトにバンドルし、マッピング ハッシュ(--map-id-template)を DEX ヘッダーに埋め込み、クラス SourceFile 属性(--source-file-template)を書き換えて、retrace がスタック トレースを自動的にシンボリック化できるようにします。

obfuscate: false(または明示的に公開 API 名を保持)は、次の場合にのみ保持します。

  • bootclasspath または system_server クラスパスの共有ライブラリ: 実行時に他のモジュール(framework.jar、services.jar、<uses-library> ターゲットなど)によって動的にリンクされる外部 API サーフェスを公開するモジュール(java_library または java_sdk_library)は、obfuscate: false を保持するか、@KeepForApi または -keep ルール(bootclasspath ターゲットの場合は protect_api_surface: true も)を使用して API サーフェスを明示的に保持する必要があります。これにより、スタブに対してコンパイルされた呼び出し元は、実行時にクラス名とメンバー名を解決できます。

アプリケーションで obfuscate: true を有効にする前に、次の移行の前提条件を確認します。

  • 外部テスト APK の依存関係: 外部 android_test APK が instrumentation_for: "MyApp" を設定し、MyApp の内部クラスまたはメソッドを直接呼び出す場合、それらのシンボルをリネームすると、テスト実行時に NoSuchMethodError または NoClassDefFoundError が発生します。テストを自己計測 APK として構造化し、MyApp.impl を静的にリンクするか、テストフックに @VisibleForTesting のアノテーションを付けるか、trace_references_from を構成(ステップ 2: テスト駆動型キープルールの移行を参照)して、テストに必要な内部シンボルを保持しながら、アプリの残りの部分を難読化することを推奨します。
  • 文字列ベースのリフレクションと JNI: モジュールがリテラル文字列名(Class.forName、getDeclaredMethod、または JNI FindClass と GetMethodID)でクラス、メソッド、フィールドを検索する場合、キープルールやアノテーションがないと、難読化によってこれらのターゲットの名前が変更され、実行時の検索が失敗します。(注: アノテーションのないリフレクションも shrink: true と optimize: true で失敗します)。これらのエントリ ポイントに Keep アノテーションのガイド(@UsesReflection、@UsedByReflection、または @UsedByNative)を付加して、R8 がそれらの名前を保持し、モジュールの残りの部分を難読化するようにします。

カスタムフラグをゼロにする原則に従う

理想的な Android プラットフォーム モジュールには、カスタムの proguard.flags ファイルや keep.xml ファイルはありません。Android プラットフォーム ビルドでは、カスタムの保持ルールのほとんどが冗長です。

  • プラットフォーム ベースラインと AAPT2: ほとんどのエントリ ポイントは、グローバル プラットフォーム ベースライン(@Keep、JNI native メソッド、@VisibleForTesting など)によって自動的に保持されるか、AndroidManifest.xml とレイアウト リソースから AAPT2 によって生成されます(デフォルトのプラットフォーム保持ルールについてを参照)。
  • 対象となる代替手段: エントリ ポイントを保持する必要がある場合は、切り離された .flags ファイルよりも、宣言サイトのアノテーション(keepanno、@VisibleForTesting)、エクスポートされたライブラリ ルール(export_proguard_flags_files: true)、静的テストのリンク設定を優先します(既存の保持ルールの監査と移行を参照)。

カスタム proguard.flags ファイルまたは keep.xml ファイルを追加または保持する前に、ルールがデフォルトのベースラインですでに処理されているか、コード アノテーションに移行できるかどうかを確認します。

デフォルトのプラットフォーム保持ルールについて

Soong は、build/soong/java/dex.go で構成された DEX コンパイル ターゲット(android_app、android_test、インストール可能な java_library モジュール)のすべての R8 呼び出しに、次のグローバル ベースライン ルールを自動的に渡します。

  • build/make/core/proguard.flags: すべてのパッケージで @com.android.internal.annotations.VisibleForTesting でアノテーションが付けられたクラスとメンバーを保持し、android.**、com.android.**、com.google.android.** パッケージ内で @VisibleForTesting(androidx.annotation.VisibleForTesting または com.google.common.annotations.VisibleForTesting)でアノテーションが付けられたクラスとメンバーを保持します。@TestApi、@Keep(androidx.annotation、android.support.annotation、com.android.internal.annotations)、@KeepForWeakReference、@WeaklyReferencedCallback、@dalvik.annotation.optimization.** も保持します。
  • build/make/core/proguard_basic_keeps.flags: スタック トレース、ランタイム可視性アノテーション(RuntimeVisible*Annotations)、Exceptions 属性と AnnotationDefault 属性、native メソッド、Serializable メンバー、@JavascriptInterface メソッド、Throwable(String) コンストラクタ、Parcelable$CREATOR フィールド、protobuf MessageLite フィールドの SourceFile 属性を保持します。
  • build/make/core/proguard/kotlin.flags: 特定の Kotlin メタアノテーション(kotlin.Metadata と kotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target})に関する無害な警告を抑制し、リリースビルドで Kotlin DebugMetadata アノテーションを削除します。
  • build/make/core/proguard/checknotnull.flags: 一般的な null チェック ヘルパー呼び出し(com.google.common.base.Preconditions.checkNotNull と dagger.internal.Preconditions.checkNotNull*)を簡潔なバイトコード null チェックに置き換えます。このファイルでは、フレームワーク API の境界を越えて明示的な例外メッセージを保持するために、意図的に Objects.requireNonNull を省略しています。
  • build/make/core/proguard/enumvalues.flags: ビルド構成で無効にされていない限り、enum タイプで values メソッドと valueOf メソッドを保持します。
  • AAPT2 によって自動生成されるルール: AAPT2 は、マージされた AndroidManifest.xml、レイアウト XML、設定 XML ファイルを検査して、登録された Activity、Service、BroadcastReceiver、ContentProvider、BackupAgent、Application、カスタム View サブクラス、Preference サブクラス、XML android:onClick メソッドごとに正確な保持ルールを生成します。

既存の保持ルールを監査して移行する

プラットフォーム リポジトリ内の既存の proguard.flags ファイルまたは keep.xml ファイルを監査するときは、次の 4 段階の階層に対して、各ルールを順番に評価します。

ステップ 1: 冗長なルールまたは古いルールを削除する

グローバル ベースライン、AAPT2 マニフェストとレイアウトのルール、R8 のデフォルトで既にカバーされているルールや、存在しなくなったクラスやパッケージを参照しているルールを削除します。

.flags ファイル内のすべてのルールが冗長である場合は、ファイルを削除し、Android.bp から proguard_flags_files を削除します。残りの optimize ブロックがデフォルト設定を再記述しているだけの場合は、冗長なブロックを削除し、bpfmt -w Android.bp でビルドファイルをフォーマットします。パッケージをクリーンアップするときは、サブディレクトリ内のコンパニオン モジュール(Kotlin ターゲット バリアントなど)が同じフラグファイルを参照しているかどうかを確認し、参照している場合はそれらを一緒に更新します。

ステップ 2: テスト駆動型保持ルールを移行する

次のいずれかのパターンを使用して、テスト駆動型の保持ルールをカスタム本番環境の .flags ファイルから移動します。

  • 実装ライブラリをテストにリンクする(static_libs): android_test がアプリのパッケージ プライベートまたは内部実装の詳細を実行する場合、推奨されるプラットフォーム アーキテクチャは、アプリのソースファイルを android_library(MyApp.impl)に配置し、MyApp.impl を MyApp と MyAppTests の両方に静的にリンクすることです。

    android_library {
        name: "MyApp.impl",
        srcs: ["src/**/*.java"],
        manifest: "AndroidManifest.xml",
    }
    
    android_app {
        name: "MyApp",
        static_libs: ["MyApp.impl"],
        optimize: {
            obfuscate: true,
            shrink_resources: true,
        },
    }
    
    android_test {
        name: "MyAppTests",
        srcs: ["tests/**/*.java"],
        static_libs: ["MyApp.impl"],
    }
    

    この 3 モジュール パターンにより、MyAppTests は内部クラスに完全にアクセスできる自己計測テストとして実行され、MyApp はテストを中断することなく obfuscate: true を自由に有効にでき、テスト専用のエントリ ポイントが本番環境の APK に含まれるのを防ぎ、テストコードが変更されたときに MyApp を再ビルドする必要がなくなります。

  • @VisibleForTesting でテストフックにアノテーションを付ける: 外部テスト APK が android_app ターゲットの少数の内部メソッドまたはコンストラクタを呼び出し、.impl ライブラリへの再構築が現実的でない場合は、それらの宣言に @VisibleForTesting でアノテーションを付けます。グローバル build/make/core/proguard.flags ベースラインは、カスタム .flags ファイルなしで、android.**、com.android.**、com.google.android.** パッケージの @VisibleForTesting アイテムを自動的に保持します。(これらの名前空間外のベンダー モジュールについては、@UsedByReflection またはエクスポートされたライブラリ ルールを使用してください)。

  • プラットフォーム ライブラリの境界を越えて trace_references_from を使用する: テストでターゲット実装を静的にリンクできない場合(システム サーバー サービスや framework-connectivity などのプラットフォーム JAR を実行するテストなど)、テストソースを含む Java ライブラリ コンパニオン モジュールを指すターゲット モジュールで trace_references_from を構成します。R8 は、コンパニオン バイトコードで参照されるすべてのクラスとメンバーをトレースして保持します。

ステップ 3: 所有ライブラリからルールをエクスポートする

共有 java_library または android_library が独自の内部リフレクションまたは JNI コールバックに保持ルールを必要とする場合は、ライブラリ ターゲットでルールを定義し、export_proguard_flags_files: true を設定します。

java_library {
    name: "my-shared-library",
    srcs: ["src/**/*.java"],
    optimize: {
        proguard_flags_files: ["proguard.flags"],
        export_proguard_flags_files: true,
    },
}

my-shared-library を静的にリンクするすべてのダウンストリーム android_app とライブラリ ターゲットは、これらのルールを自動的に継承するため、ダウンストリーム アプリでルールを複製する必要はありません。異なるディレクトリにまたがる複数のモジュールが、単一のコード ライブラリに関連付けられていないルールセットを共有する必要がある場合は、Android.bp の明示的な filegroup を介して .flags ファイルを公開し、モジュールがパッケージ境界を越えて :my-shared-flags をクリーンに参照できるようにします。ダウンストリーム アプリの最適化を制限せずにライブラリ コンシューマーの keep ルールを作成する一般的なガイダンスについては、ライブラリ作成者向けの最適化をご覧ください。

ステップ 4: ソースコード内の宣言にアノテーションを付ける

クラス、メソッド、コンストラクタ、フィールドがリフレクションまたは JNI を介してアクセスされ、AAPT2 またはグローバル ベースラインでカバーされていない場合は、.flags ファイル内の分離された -keep ルールを、Keep アノテーションのガイドのソース アノテーションに置き換えます(keepanno Javadoc リファレンスを参照)。

  • @UsesReflection: リフレクションを実行するコードを制御する場合は、このアノテーションを使用します。リフレクション呼び出しサイトに配置して、動的にアクセスされるターゲット クラス、メソッド、フィールドを宣言します。@UsesReflection は、アノテーション付きの呼び出しサイト自体が到達可能であるという前提条件を自動的にエンコードするため、呼び出しサイトが使用されていない場合、R8 は呼び出し元とリフレクション ターゲットの両方をプルーニングします。
  • @UsedByReflection: Bundle または Settings キーから Class.forName で読み込まれたクラス、動的クラスローダー間で読み込まれたプラグイン クラスなど、外部コードまたはライブラリによってリフレクションでインスタンス化または呼び出しされるクラス、メソッド、フィールド、コンストラクタに配置します。kind(KeepItemKind.CLASS_AND_METHODS など)、preconditions(@KeepCondition)、パラメータ制約を指定して、R8 が正確なリフレクション契約のみを保持し、クラスの未使用メンバーを最適化または削除できるようにします。AAPT2 はマニフェスト登録済みコンポーネント(JobService や BroadcastReceiver など)をすでに保持しているため、それらに @UsedByReflection を追加しないでください。
  • @UsedByNative: GetMethodID、GetStaticMethodID、GetFieldID を使用して C または C++ JNI コードからアクセスされるメソッドまたはフィールドに配置します。
  • @KeepForApi: 配布前にライブラリ自体を縮小する際に、そのまま残しておく必要があるライブラリ API クラスまたはメンバーに配置します。
  • @Keep(androidx.annotation.Keep): keepanno が適用されない場合のフォールバックとして使用します。クラスに @Keep を適用すると、クラスとそのすべてのメンバーが無条件で保持されます。メソッドまたはフィールドに @Keep を適用すると、クラスがインスタンス化されなくても、メンバーとその包含クラスを保持する無条件のエントリ ポイントとして機能します。一方、keepanno は条件付きの到達可能性を表します。

Soong モジュールで R8 keepanno アノテーションを使用するには:

  1. Android.bp の libs に "keepanno-annotations" を追加します。

    libs: [
        "keepanno-annotations",
    ],
    
  2. com.android.tools.r8.keepanno.annotations.* をインポートし、Java または Kotlin のソースコードで宣言または呼び出しサイトにアノテーションを付けます。

    import com.android.tools.r8.keepanno.annotations.KeepItemKind;
    import com.android.tools.r8.keepanno.annotations.UsedByReflection;
    
    public final class CustomPluginController {
        @UsedByReflection(
            description = "Instantiated via Class.forName from plugin config",
            kind = KeepItemKind.CLASS_AND_METHODS)
        public CustomPluginController(Context context) {
            // ...
        }
    }
    

    R8 は keepanno アノテーションを内部の保持ルールモデルに直接変換し、最終的な DEX 出力からアノテーションを削除します。これにより、実行時のバイトコードのオーバーヘッドがゼロになります。

よくある問題の回避

以降のセクションでは、プラットフォーム コードでよくある proguard.flags と keep.xml の落とし穴と、その修正方法について説明します。

Broad コンポーネントの keep ルール

# Don't do this:
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
  • パフォーマンスに影響する理由: このルールは AAPT2 と完全に重複しています。ワイルドカードを使用しているため、コンポーネントが最終的にマージされた AndroidManifest.xml に登録されているかどうかを確認せず、R8 にクラスパス上のどこかに見つかったすべての Activity、Service、BroadcastReceiver サブクラス(未使用のライブラリ コンポーネントや無効なデバッグ アクティビティを含む)を保持させます。
  • 推奨される修正: ルールを削除します。AAPT2 はマージ マニフェストを検査し、登録されたコンポーネントの正確な -keep ルールを生成します。

Broad XML クリック ハンドラの保持ルール

# Don't do this:
-keepclassmembers class * {
    public void *(android.view.View);
}
  • パフォーマンスが低下する理由: モジュール内のすべてのクラスで public void *(View) メソッドを保持すると、R8 は 1 つの View パラメータを受け入れるメソッドを削除したりインライン化したりできなくなります。
  • 推奨される修正: ルールを削除します。AAPT2 はレイアウト XML ファイルをスキャンし、android:onClick 属性で参照されるメソッドのターゲット保持ルールを生成します。

Broad View のゲッターとセッターの保持ルール

# Don't do this:
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(...);
    public *** get*();
}
  • パフォーマンスに悪影響を及ぼす理由: このルールにより、R8 はアプリ内のすべての View サブクラスと、すべてのリンクされたライブラリ(AndroidX ライブラリや Material ライブラリなど)のすべてのゲッターとセッターを保持する必要があり、UI コード全体でデッド メソッドの削除とインライン化がブロックされます。
  • 推奨される修正: ルールを削除します。AAPT2 は、レイアウト XML ファイルから拡張されたカスタム View クラスのコンストラクタをすでに保持しています。コードが ObjectAnimator.ofFloat(view, "translationZ", ...) などのリフレクション文字列名を使用してビュー プロパティをアニメーション化する場合は、リフレクションを完全に回避するために、文字列名を型付きプロパティ参照(View.TRANSLATION_Z またはカスタムの FloatProperty または IntProperty 実装)に置き換えます。リフレクション プロパティ アクセスを回避できない場合は、特定のゲッターまたはセッターに @UsedByReflection のアノテーションを付けます。

包括的な最適化または難読化が無効になっている

# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
  • パフォーマンスが低下する理由: .flags ファイル内に -dontoptimize または -dontshrink を配置すると、モジュールの Android.bp 設定が暗黙的にオーバーライドされ、ターゲット全体で最適化パスが無効になります。さらに、ライブラリが -dontoptimize を含む .flags ファイルをエクスポートすると、そのライブラリをリンクするすべてのダウンストリーム android_app で R8 最適化が無効になります。
  • 推奨される修正: .flags ファイルから -dontoptimize、-dontshrink、-dontobfuscate を削除します。リーフ ターゲットの optimize ブロック(shrink、optimize、obfuscate)を使用して、Android.bp で最適化動作を明示的に制御します。

コミットされたルールの診断フラグ

# Don't do this in proguard.flags:
-verbose
-printmapping proguard.map
-printusage usage.txt
-printconfiguration config.txt
  • ビルドに悪影響を及ぼす理由: 診断フラグがビルドログを氾濫させるか、サンドボックス化された Soong ビルド中にローカルパスに出力ファイルを書き込もうとする。
  • 推奨の修正方法: コミットされた .flags ファイルからこれらのフラグを削除します。Soong は、proguard_dictionary や proguard_usage.zip などの R8 マッピングと使用状況の出力を、out/soong/.intermediates/ の下のモジュールの中間ディレクトリに自動的に書き込みます。

テストコードの保持ルール

  • パフォーマンスが低下する理由: テストアクセス専用のカスタム -keep ルールを追加すると、そのコードは難読化されず、本番環境のビルドに保持されます。@VisibleForTesting でメソッドにアノテーションを付けるか、trace_references_from を構成すると、手動の -keep ルールを維持する必要がなくなりますが、これらのシンボルは引き続き本番環境バイナリで出荷されます。
  • 推奨される修正: テスト専用のエントリ ポイントを本番環境の APK から完全に除外するには、ステップ 2: テスト駆動型の保持ルールの移行で説明されているように、アプリケーションを .impl ライブラリに構造化し、static_libs を使用して自己インストルメント化テスト APK にリンクします。

keep.xml のリソースのワイルドカード

<!-- Don't do this in res/raw/keep.xml: -->
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@raw/*,@drawable/*,@string/*" />
  • パフォーマンスに悪影響を及ぼす理由: 包括的なワイルドカードは、モジュールとその依存関係全体で一致するタイプのすべてのリソースを保持するため、リソースの縮小(shrink_resources: true)が無効になり、APK とマッピングされた resources.arsc テーブルが肥大化します。
  • 推奨される修正:
    • keep.xml よりも静的リソース参照を優先する: 番号付きのテスト文字列やテーマ付きのドローアブルなど、リソースの境界付きセットを動的に検索するために Resources.getIdentifier メソッドを使用することは避けてください。代わりに、コンパイル時の switch ステートメントを使用するか、静的な R.id、R.string、R.drawable 定数をマッピングします。静的参照を使用すると、R8 と AAPT2 は正確なライブ リソースをトレースし、実行時の文字列ルックアップのオーバーヘッドを排除し、keep.xml の必要性を完全に排除できます。
    • 特定のリソース名を一覧表示する: サードパーティのライセンス リーダーなどによって動的ルックアップが必要な場合は、tools:keep に正確なリソース識別子(tools:keep="@raw/third_party_licenses" など)を一覧表示します。
    • 冗長な keep.xml ファイルを削除する: リストされているリソースがコード(R.raw.foo)または XML(@raw/foo)で静的に参照されている場合、シュリンカーはそれらを自動的に保持するため、res/raw/keep.xml を削除できます。

ルールの変更を確認して監査する

proguard.flags または keep.xml で保持ルールを削除または絞り込む場合は、モジュールがクリーンにビルドされ、単体テストと計測テストに合格し、必要なエントリ ポイントをすべて保持していることを確認します。

モジュールをビルドしてテストする

  1. モジュールをクリーンビルドして、R8 と AAPT2 が参照欠落の警告なしで完了することを確認します。

    m <MODULE_NAME>
    
  2. atest を使用してモジュールの単体テストとインストルメンテーション テストを実行します。

    atest <TEST_MODULE_NAME>
    
  3. システムアプリ、特権サービス、ハードウェア依存モジュールの場合、ターゲットの物理デバイスまたはデバイス テストラボでインストルメンテーション テストと UI テストを実行して、ランタイム リフレクション パス、IPC バインディング、リソース インフレーションをテストします。

DEX とリソースの差分を検査する

コンパイル済みの APK または JAR をルールの変更前と変更後で比較し、R8 が想定されるエントリ ポイントを削除せずにデッドコードとリソースを削除することを確認します。

  • apkanalyzer または dexdump を使用して、出力 APK ファイルまたは DEX ファイルで保持されているクラス、メソッド、フィールドを検査します。

    apkanalyzer dex packages $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • keep.xml を変更したり、shrink_resources を有効にしたりする場合は、aapt2 dump resources を使用して、ビルドが未使用のリソースを削除し、必要なリソースを保持していることを確認します。

    aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    

R8 で Keep 半径とルールの包含を分析する

オープンソースの R8 コンパイラには、コンパイル中にすべての Keep ルールの正確な影響を測定する Keep Radius アナライザ(Android Studio と Gradle では R8 構成アナライザとしても公開されています)が含まれています。Soong はこのアナライザを Android プラットフォーム ビルドに直接統合します(build/soong/java/dex.go で構成)。

プラットフォーム モジュールまたはビルド全体で保持ルールを分析するには:

  1. R8_DUMP_KEEP_RADIUS=true 環境変数を使用してビルドを実行します。

    R8_DUMP_KEEP_RADIUS=true m <MODULE_NAME>
    

    R8_DUMP_KEEP_RADIUS=true が設定されている場合、Soong は R8 に、out/soong/.intermediates/ の下にあるコンパイル済みモジュールごとに、中間 r8keepradius.pb ファイルにキープルールの指標を記録するよう指示します。R8 は、各保持ルールと keepanno アノテーションについて、次の情報を記録します。

    • 即時保持半径: ルールによって保持される正確なクラス、フィールド、メソッドと、シュリンク、最適化、難読化に対して適用される特定の制約。
    • ルールの包含: どの他の保持ルールまたはアノテーションが同じアイテムをすでに保持しているか。カスタムルールが AAPT2 ルールまたはグローバル ベースライン ルールに完全に包含されている場合は、安全に削除できます。
    • パッケージ全体とグローバル ルール: パッケージ全体の広範なワイルドカードを使用するルール、またはグローバル構成ディレクティブを適用するルール。
  2. KeepRadiusHtmlReportGenerator(prebuilts/r8/r8.jar にバンドル)を使用して、r8keepradius.pb の出力をインタラクティブな HTML レポートに変換します。

    # Generate an HTML report for a single module
    INTERMEDIATES=out/soong/.intermediates/packages/apps/<APP_NAME>/<APP_NAME>
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        $INTERMEDIATES/android_common/r8keepradius.pb \
        keep_radius_report.html
    
    # Scan all built modules under out/soong/.intermediates and generate
    # per-module HTML reports plus an aggregate keepradius.html summary
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        out/soong/.intermediates \
        out/keep_radius_reports
    

    ディレクトリが指定されると、KeepRadiusHtmlReportGenerator はすべての *keepradius*.pb ファイルを走査し、各モジュールの HTML レポートを生成して、ビルド全体でライブアイテム数、保持アイテム数、最大半径の保持ルールをまとめた out/keep_radius_reports/keepradius.html を作成します。生成されたレポートの圧縮、最適化、難読化のスコアの解釈について詳しくは、R8 構成アナライザを使用するをご覧ください。

Soong は、build/soong/java/dex.go で次の 2 つの R8 診断環境変数もサポートしています。

  • R8_DUMP_INPUT=true: スタンドアロン R8 再現用のすべての入力 JAR、ライブラリ JAR、マージされた ProGuard 構成を含むモジュールの中間ディレクトリに r8inputs.zip を書き込みます。
  • R8_DUMP_PERFETTO_TRACE=true: Perfetto で R8 コンパイル パスを検査するために、r8trace.ptrace をモジュールの中間ディレクトリに書き込みます。

ライブラリ コンシューマー ルールを検証する

java_library または android_library のエクスポートでルールがダウンストリーム コンシューマー(export_proguard_flags_files: true)に保持される場合、それらのルールには、コンシューマー アプリの最適化を変更または無効にするグローバル フラグを含めてはなりません。

R8 は、オープンソースの keep ルール アナライザ(ProcessKeepRules)を提供します。これは、Android プラットフォーム ビルドで process-keep-rules ホストツール(prebuilts/r8/Android.bp で定義)として公開されています。

# Build the R8 keep rules validator host binary
m process-keep-rules

# Validate one or more ProGuard configuration files
out/host/linux-x86/bin/process-keep-rules <PROGUARD_FLAGS_PATH>

process-keep-rules ツールは各構成ファイルを解析し、ライブラリ コンシューマー ルールで許可されていないディレクティブを検出すると、ファイルと行の診断で失敗します。許可されないディレクティブには、グローバル最適化、縮小、難読化の無効化(-dontoptimize や -dontshrink など)、パッケージの再パッケージ化とアクセス変更フラグ、診断またはマッピング フラグ、アプリレベルの -keepattributes などがあります。

pgaudit.py でソースツリーを監査する

R8 のビルド時アナライザとともにビルドされていないソース ディレクトリを監査するために、Android プラットフォーム ツリーには build/make/core/proguard/tools/ に pgaudit.py スクリプトが含まれています。この静的分析ツールは、リポジトリ全体で Android.bp、proguard.flags、keep.xml ファイルをスキャンし、AAPT2 またはグローバル プラットフォームのベースライン、広範なワイルドカード、-dontoptimize オーバーライド、テスト専用の keep ルールですでにカバーされているルールにフラグを設定します。

# Audit a single package directory
./build/make/core/proguard/tools/pgaudit.py packages/apps/Provision/

# Scan a subsystem tree and write a structured JSON report
./build/make/core/proguard/tools/pgaudit.py packages/ --json=audit_report.json

プラットフォーム チームと OEM チームは、pgaudit.py を組み合わせてソースツリーのトリアージを迅速に行い、process-keep-rules を使用してエクスポートされたライブラリ ルールを検証し、R8_DUMP_KEEP_RADIUS=true と KeepRadiusHtmlReportGenerator を組み合わせてビルド内のすべてのルールの正確なクラス、フィールド、メソッドの保持半径を測定できます。