アプリの圧縮、難読化、最適化

アプリのサイズをできる限り小さくするには、リリースビルドで「圧縮」を有効にして、使用されていないコードとリソースを削除する必要があります。圧縮を有効にすると、アプリのクラスとメンバーの名前を短くする「難読化」と、より積極的な戦略を適用してアプリのサイズをさらに小さくする「最適化」によるメリットも得られます。このページでは、R8 でプロジェクトのコンパイル時タスクを実行する方法と、それらのタスクをカスタマイズする方法について説明します。

Android Gradle プラグイン 3.4.0 以上を使用したプロジェクトのビルドでは、ProGuard によるコンパイル時のコード最適化が行われません。その代わり、R8 コンパイラとの連携により、コンパイル時に以下のタスクが実行されます。

  • コードの圧縮(ツリー シェイキング): 使用されていないクラス、フィールド、メソッド、属性を検出し、アプリとそのライブラリ依存関係から安全に削除します(64K 参照制限を回避する有用なツールです)。たとえば、ライブラリ依存関係の API を数個だけ使用している場合、圧縮タスクはアプリが使用していないライブラリ コードを特定し、アプリからそのコードだけを削除できます。詳しくは、コードの圧縮をご覧ください。
  • リソースの圧縮: パッケージ化アプリから使用されていないリソース(アプリのライブラリ依存関係で使用されていないリソースなど)を削除します。リソースの圧縮はコードの圧縮と連携して機能するため、使用されないコードが削除されると、参照されなくなったリソースも安全に削除できます。詳しくは、リソースの圧縮をご覧ください。
  • 難読化: クラスとメンバーの名前を短くし、DEX ファイルのサイズを小さくします。詳しくは、コードの難読化をご覧ください。
  • 最適化: コードを検査して書き換えることで、アプリの DEX ファイルのサイズをさらに小さくします。たとえば、特定の if / else ステートメントの else {} 分岐を絶対に通らないことが R8 によって検出された場合、R8 はその else {} 分岐のコードを削除します。詳しくは、コードの最適化をご覧ください。

アプリのリリース バージョンをビルドするときに、上記のコンパイル時タスクを実行するように R8 を設定できます。ProGuard ルールファイルを使用して、特定のタスクを無効にしたり、R8 の動作をカスタマイズしたりすることもできます。実際には、R8 は既存のすべての ProGuard ルールファイルと連携するため、R8 を使用するように Android Gradle プラグインを更新しても、既存のルールを変更する必要はありません。

圧縮、難読化、最適化の有効化

Android Studio 3.4 または Android Gradle プラグイン 3.4.0 以上を使用する場合、R8 がデフォルトのコンパイラになります。R8 はプロジェクトの Java バイトコードを Android プラットフォームで実行可能な DEX 形式に変換します。ただし、Android Studio を使用して新しいプロジェクトを作成する場合、圧縮、難読化、コードの最適化はデフォルトでは有効になりません。これは、こうしたコンパイル時の最適化によってプロジェクトのビルド時間が長くなり、保持するコードのカスタマイズが十分でない場合にバグが発生する可能性があるためです。

そのため、これらのコンパイル時のタスクは、公開前にテストするアプリの最終版をビルドするときに有効にすることをおすすめします。圧縮、難読化、最適化を有効にするには、プロジェクト レベルのビルド スクリプトに以下を含めます。

Kotlin

android {
    buildTypes {
        getByName("release") {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type. Make sure to use a build
            // variant with `isDebuggable=false`.
            isMinifyEnabled = true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            isShrinkResources = true

            // Includes the default ProGuard rules files that are packaged with
            // the Android Gradle plugin. To learn more, go to the section about
            // R8 configuration files.
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    ...
}

Groovy

android {
    buildTypes {
        release {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type. Make sure to use a build
            // variant with `debuggable false`.
            minifyEnabled true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            shrinkResources true

            // Includes the default ProGuard rules files that are packaged with
            // the Android Gradle plugin. To learn more, go to the section about
            // R8 configuration files.
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

R8 の構成ファイル

R8 は ProGuard ルールファイルを通じて、そのデフォルトの動作を変更したり、アプリの構造(アプリのコードへのエントリ ポイントとして機能するクラスなど)を正確に把握したりできます。これらのルールファイルの一部は変更できますが、ルールにはコンパイル時ツール(AAPT2 など)によって自動的に生成されるものや、アプリのライブラリ依存関係から継承されるものもあります。次の表で、R8 が使用する ProGuard ルールファイルのソースについて説明します。

ソース 場所 説明
Android Studio <module-dir>/proguard-rules.pro Android Studio を使用して新しいモジュールを作成すると、IDE がそのモジュールのルート ディレクトリに proguard-rules.pro ファイルを作成します。

デフォルトでは、このファイルはルールを適用しません。そのため、カスタムの keep ルールなどの独自の ProGuard ルールをこのファイルに含めます。

Android Gradle プラグイン コンパイル時に Android Gradle プラグインによって生成されます。 Android Gradle プラグインは proguard-android-optimize.txt を生成します。このファイルには多くの Android プロジェクトにとって有用なルールが記載されます。また、このファイルによって @Keep* アノテーションが有効になります。

デフォルトでは、Android Studio を使用して新しいモジュールを作成する際、モジュール レベルのビルド スクリプトによって、このルールファイルがリリースビルドに追加されます。

注: Android Gradle プラグインには事前定義された追加の ProGuard ルールファイルが含まれていますが、proguard-android-optimize.txt を使用することをおすすめします。

ライブラリ依存関係 AAR ライブラリ: <library-dir>/proguard.txt

JAR ライブラリ: <library-dir>/META-INF/proguard/

AAR ライブラリが独自の ProGuard ルールファイルで公開される場合、コンパイル時の依存関係としてその AAR を含めると、プロジェクトのコンパイル時にそのルールが R8 によって自動的に適用されます。

AAR ライブラリにパッケージ化されているルールファイルを使用すると、ライブラリが正常に機能するために特定の keep ルールが必要な場合(ライブラリのデベロッパーがトラブルシューティング手順を実施している場合)に役立ちます。

ただし、ProGuard ルールは追加型であるため、AAR ライブラリの依存関係に含まれる特定のルールは削除できず、アプリの他の部分のコンパイルに影響する可能性があるので注意してください。たとえば、ライブラリにコードの最適化を無効にするルールが含まれている場合、そのルールによってプロジェクト全体の最適化が無効になります。

Android Asset Package Tool 2(AAPT2) minifyEnabled true と指定してプロジェクトをビルドした後: <module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt AAPT2 は、アプリのマニフェスト、レイアウト、その他のアプリのリソースでのクラスへの参照に基づいて keep ルールを生成します。たとえば AAPT2 には、デベロッパーがアプリのマニフェストでエントリ ポイントとして登録した各アクティビティの keep ルールが含まれます。
カスタム構成ファイル Android Studio を使用して新しいモジュールを作成すると、デフォルトで IDE によって作成される <module-dir>/proguard-rules.pro に、独自のルールを追加できるようになります。 追加の構成を含めることができ、R8 がコンパイル時にその構成を適用します。

minifyEnabled プロパティを true に設定すると、上記の使用可能なすべてのソースのルールが R8 によって結合されます。他のコンパイル時の依存関係(ライブラリの依存関係など)により、デベロッパーが把握していない R8 の動作変更が行われる可能性があるため、R8 でのトラブルシューティングの際にはこの点に注意してください。

R8 がプロジェクトのビルド時に適用するすべてのルールの完全なレポートを出力するには、モジュールの proguard-rules.pro ファイルに以下を追加します。

// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt

構成の追加

Android Studio を使用して新しいプロジェクトまたはモジュールを作成すると、IDE によって作成される <module-dir>/proguard-rules.pro に独自のルールを追加できるようになります。他のファイルからルールを追加するには、モジュールのビルド スクリプトの proguardFiles プロパティにルールを追加します。

たとえば、対応する productFlavor ブロックに別の proguardFiles プロパティを追加することで、各ビルド バリアントに固有のルールを追加できます。次の Gradle ファイルで、flavor2-rules.proflavor2 プロダクト フレーバーに追加します。release ブロックのルールも適用されるため、flavor2 で 3 つの ProGuard ルールがすべて使用されます。

さらに、testProguardFiles プロパティを追加して、テスト APK のみに含まれる ProGuard ファイルのリストを指定することもできます。

Kotlin

android {
    ...
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                "proguard-rules.pro"
            )
            testProguardFiles(
                // The proguard files listed here are included in the
                // test APK only.
                "test-proguard-rules.pro"
            )
        }
    }
    flavorDimensions.add("version")
    productFlavors {
        create("flavor1") {
            ...
        }
        create("flavor2") {
            proguardFile("flavor2-rules.pro")
        }
    }
}

Groovy

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                'proguard-rules.pro'
            testProguardFiles
                // The proguard files listed here are included in the
                // test APK only.
                'test-proguard-rules.pro'
        }
    }
    flavorDimensions "version"
    productFlavors {
        flavor1 {
            ...
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

コードの圧縮

minifyEnabled プロパティを true に設定すると、R8 によるコードの圧縮がデフォルトで有効になります。

コードの圧縮(ツリー シェイキングとも呼ばれています)とは、ランタイムに必要のないコードを R8 が判断して削除するプロセスです。たとえば、アプリに多数のライブラリ依存関係が含まれていても、その機能のごく一部しか使用されない場合、このプロセスによってアプリのサイズを大幅に縮小できます。

アプリのコードを圧縮するにあたり、R8 はまず、一連の構成ファイルを基に、アプリのコードへのエントリ ポイントをすべて特定します。これらのエントリ ポイントには、Android プラットフォームがアプリのアクティビティやサービスを開始するために使用する可能性があるクラスがすべて含まれます。R8 は各エントリ ポイントからアプリのコードを検査し、アプリがランタイムにアクセスする可能性があるすべてのメソッド、メンバー変数、その他のクラスのグラフを作成します。このグラフに接続されないコードは「到達不能」とみなされ、アプリから削除できます。

図 1 は、ランタイム ライブラリの依存関係を持つアプリを示します。R8 はアプリのコードの検査中に、foo()faz()bar() の各メソッドが MainActivity.class エントリ ポイントから到達可能と判断します。一方、クラス OkayApi.class またはそのメソッド baz() はランタイムにアプリで使用されないため、アプリの圧縮時にそのコードが R8 によって削除されます。

図 1. R8 はコンパイル時に、プロジェクトに結合されている keep ルールに基づいてグラフを作成し、到達不能なコードを特定します。

R8 は、プロジェクトの R8 構成ファイル-keep ルールを通じてエントリ ポイントを特定します。つまり、アプリの圧縮時に R8 が破棄してはならないクラスを keep ルールで指定すると、R8 はそれらのクラスをアプリへの想定されるエントリ ポイントとみなします。Android Gradle プラグインと AAPT2 は、ほとんどのアプリ プロジェクト(アプリのアクティビティ、ビュー、サービスなど)について必要な keep ルールを自動的に生成します。ただし、他にも keep ルールを追加してこのデフォルトの動作をカスタマイズする必要がある場合は、保持するコードのカスタマイズをご覧ください。

カスタマイズではなく、アプリのリソースのサイズを小さくすることにのみ関心がある場合は、リソースの圧縮にお進みください。

保持するコードのカスタマイズ

使用されていないコードのみを R8 で削除する場合、通常はデフォルトの ProGuard ルールファイル(proguard-android- optimize.txt)で十分です。ただし、R8 で正確に分析することが困難な場合もあり、アプリが実際に必要とするコードが削除されてしまうこともあります。以下のようなケースでは、R8 が誤ってコードを削除する可能性があります。

  • アプリが Java Native Interface(JNI)からメソッドを呼び出す場合
  • アプリが実行時にコードを検索する場合(リフレクションの使用時など)

アプリをテストすることで不適切に削除されたコードに起因するエラーが明らかになりますが、削除されたコードのレポートを生成すると、どのコードが削除されたかを調べることができます。

エラーを修正し、R8 が特定のコードを削除しないようにするには、ProGuard ルールファイルに -keep の行を追加します。次に例を示します。

-keep public class MyClass

また、保持するコードに @Keep アノテーションを追加することもできます。クラスに @Keepを追加すると、クラス全体がそのまま保持されます。このアノテーションをメソッドまたはフィールドに追加すると、メソッドまたはフィールド(およびその名前)に加え、クラス名もそのまま保持されます。このアノテーションを使用できるのは、AndroidX Annotations Library を使用している場合と、圧縮の有効化で説明したように、Android Gradle プラグインにパッケージ化されている ProGuard ルールファイルを追加した場合だけです。

-keep オプションを使用する場合は、さまざまな点について考慮する必要があります。ルールファイルのカスタマイズについて詳しくは、ProGuard マニュアルをご覧ください。トラブルシューティングのセクションに、コードを削除したときに発生する可能性があるその他の一般的な問題について概説されています。

ネイティブ ライブラリのストリップ

アプリのリリースビルド内のネイティブ コード ライブラリは、デフォルトでストリップされます。これにより、アプリで使用されるネイティブ ライブラリに含まれるシンボル テーブルとデバッグ情報が削除されます。ネイティブ コード ライブラリをストリップすると、サイズが大幅に削減されますが、情報(クラス名、関数名など)がないため、Google Play Console でクラッシュを診断できなくなります。

ネイティブ コードでのクラッシュのサポート

Google Play Console の Android Vitals で、ネイティブ コードでのクラッシュが報告されます。簡単な手順で、アプリのネイティブ デバッグ シンボル ファイルを生成してアップロードできます。このファイルがあると、ネイティブ コードでのクラッシュのスタック トレースに対して Android Vitals でシンボリケーション(クラス名や関数名を含む)が可能になり、本番環境でアプリをデバッグできます。この手順は、プロジェクトで使用される Android Gradle プラグインのバージョンと、プロジェクトのビルド出力によって異なります。

Android Gradle プラグイン バージョン 4.1 以降

プロジェクトで Android App Bundle をビルドする場合は、その中にネイティブ デバッグ シンボル ファイルを自動的に含めることができます。このファイルをリリースビルドに含めるには、アプリの build.gradle.kts ファイルに次の行を追加します。

android.buildTypes.release.ndk.debugSymbolLevel = { SYMBOL_TABLE | FULL }

以下から、デバッグ シンボルのレベルを選択します。

  • SYMBOL_TABLE を使用して Play Console のシンボリケーション スタック トレース内の関数名を取得します。 このレベルは Tombstone に対応しています。
  • FULL を使用して Play Console のシンボリケーション スタック トレース内の関数名、ファイル、行番号を取得します。

プロジェクトで APK をビルドする場合は、前述の build.gradle.kts ビルド設定を使用して、ネイティブ デバッグ シンボル ファイルを別個に生成します。手動で Google Play Console にネイティブ デバッグ シンボル ファイルをアップロードします。Android Gradle プラグインはビルドプロセスの一環として、このファイルをプロジェクトの次の場所に出力します。

app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip

Android Gradle プラグイン バージョン 4.0 以前(とその他のビルドシステム)

Android Gradle プラグインはビルドプロセスの一環として、ストリップしていないライブラリのコピーをプロジェクト ディレクトリに保持します。 このディレクトリ構造は次のようになります。

app/build/intermediates/cmake/universal/release/obj/
├── armeabi-v7a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── arm64-v8a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── x86/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
└── x86_64/
    ├── libgameengine.so
    ├── libothercode.so
    └── libvideocodec.so
  1. 次のようにこのディレクトリの内容を圧縮します。

    cd app/build/intermediates/cmake/universal/release/obj
    zip -r symbols.zip .
    
  2. Google Play Console に手動で symbols.zip ファイルをアップロードします。

リソースの圧縮

リソースの圧縮は、必ずコードの圧縮と連動して機能します。コード圧縮ツールによって未使用のコードがすべて削除された後、リソース圧縮ツールで、アプリがまだ使用するリソースを特定できます。これは、リソースが含まれるコード ライブラリを追加した場合に特に当てはまります。使用されないライブラリ コードを削除すると、ライブラリ リソースが参照されなくなり、リソース圧縮ツールでライブラリ リソースを削除できるようになります。

リソース圧縮を有効にするには、ビルド スクリプトで shrinkResources プロパティを true に設定します(コード圧縮の minifyEnabled も一緒に設定します)。次に例を示します。

Kotlin

android {
    ...
    buildTypes {
        getByName("release") {
            isShrinkResources = true
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Groovy

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android.txt'),
                'proguard-rules.pro'
        }
    }
}

コード圧縮用の minifyEnabled を使ってアプリをまだビルドしていない場合は、先にビルドを試してから shrinkResources を有効にしてください。これは、リソースの削除を開始する前に、動的に作成または呼び出されるクラスまたはメソッドを保持するために proguard-rules.pro ファイルの編集が必要になることがあるからです。

保持するリソースのカスタマイズ

特定のリソースを保持または破棄したい場合、<resources> タグを含める XML ファイルをプロジェクト内に作成し、保持する各リソースを tools:keep 属性で、破棄する各リソースを tools:discard 属性で指定します。どちらの属性も、リソース名をカンマ区切りのリストで指定できます。また、アスタリスク文字をワイルドカードとして使用できます。

次に例を示します。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

このファイルをプロジェクト リソースに保存します(例: res/raw/keep.xml)。このファイルは、ビルドでアプリにパッケージ化されません。

リソースを直接削除できるのに、そうせずに破棄するリソースを指定することは意味がないように思えますが、この作業はビルド バリアントを使用するときに役に立つ場合があります。たとえば、あるリソースがコードで使用されていても(したがって、圧縮ツールによって削除されなくても)、実際は特定のビルド バリアントでは使用されないことがわかっている場合、すべてのリソースを共通のプロジェクト ディレクトリに格納してから、各ビルド バリアントごとに別の keep.xml ファイルを作成することが考えられます。また、ビルドツールが誤ってリソースを必要であると識別する可能性もあります。これは、コンパイラがリソース ID をインラインで追加したことで、元来参照されるリソースと、偶然同じ値になったコード内の整数値の違いをリソース アナライザが認識できないことが原因で発生する可能性があります。

厳密な参照チェックの有効化

通常、リソース圧縮ツールは、リソースが使用されるかどうかを正確に特定できます。ただし、コードが Resources.getIdentifier() を呼び出す場合(または AppCompat ライブラリなど、ライブラリがこの呼び出しを行う場合)、コードは動的に生成される文字列に基づいてリソース名を検索することになります。このような場合、リソース圧縮ツールはデフォルトで慎重を期して、名前の形式が一致するすべてのリソースを、使用される可能性があるリソースとしてマークし、それらのリソースを削除できなくします。

たとえば次のコードでは、img_ 接頭辞が付いたすべてのリソースが、使用されるリソースとしてマークされます。

Kotlin

val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)

Java

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

また、リソース圧縮ツールはコード内のすべての文字列定数と各種の res/raw/ リソースを調べて、file:///android_res/drawable//ic_plus_anim_016.png と類似の形式のリソース URL を検索します。このツールが、このような文字列、またはこのような URL の作成に使用されそうな他の文字列を検出した場合は削除しません。

これらは、デフォルトで有効になっている安全な圧縮モードの例です。ただし、この「安全第一」の処理を無効にして、リソース圧縮ツールで、確実に使用されるリソースのみを保持するように指定できます。そのためには、次のように keep.xml ファイルで shrinkModestrict に設定します。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

厳密な圧縮モードを有効にしていて、上記のような、動的に生成される文字列のリソースをコードが参照する場合は、tools:keep 属性を使用して、これらのリソースを手動で保持する必要があります。

未使用の代替リソースの削除

Gradle のリソース圧縮ツールは、アプリのコードが参照しないリソースのみを削除します。つまり、さまざまなデバイス設定の代替リソースは削除されません。必要に応じて、Android Gradle プラグインの resConfigs プロパティを使用すると、アプリが必要としない代替リソース ファイルを削除できます。

たとえば、言語リソースを含むライブラリ(AppCompat や Google Play 開発者サービスなど)を使用している場合、アプリの残りの部分が同じ言語に翻訳されるかどうかに関係なく、アプリには、これらのライブラリにあるメッセージのすべての翻訳言語の文字列が含まれます。アプリが公式にサポートする言語のみを保持する場合は、resConfig プロパティを使用してそれらの言語を指定できます。指定されていない言語のリソースは削除されます。

次のスニペットは、言語リソースを英語とフランス語のみに限定する方法を示します。

Kotlin

android {
    defaultConfig {
        ...
        resourceConfigurations.addAll(listOf("en", "fr"))
    }
}

Groovy

android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}

Android App Bundle 形式でアプリをリリースする場合、デフォルトでは、ユーザーのデバイスに設定されている言語だけがアプリのインストール時にダウンロードされます。同様に、デバイスの画面密度に一致するリソースと、デバイスの ABI に一致するネイティブ ライブラリだけがダウンロードに含まれます。詳しくは、Android App Bundle の構成をご覧ください。

APK を使用して旧式アプリ(2021 年 8 月より前に作成)をリリースする場合は、それぞれが異なるデバイス設定を対象とする複数の APK をビルドすることで、APK に含める画面密度や ABI リソースをカスタマイズできます。

重複リソースの結合

Gradle はさらにデフォルトで、異なるリソース フォルダに存在する同じ名前のドローアブルなど、同一の名前を持つリソースを結合します。この動作は、shrinkResources プロパティによって制御することも、無効にすることもできません。これは、コードが検索している名前に一致するリソースが複数あったときのエラーを回避するために必要な動作です。

リソースの結合は、2 つ以上のファイルが同一のリソース名、タイプ、修飾子を共有している場合にのみ行われます。Gradle は(下記の優先順位に基づいて)、重複ファイルのうち最適と判断したファイルを選択し、最終的なアーティファクトでの配布用にそのリソースひとつだけを AAPT に渡します。

Gradle は次の場所で重複リソースを検索します。

  • メインリソース。メイン ソースセットと関連付けられていて、通常は src/main/res/ にあります。
  • ビルドタイプとビルド フレーバーからなるバリアント オーバーレイ。
  • ライブラリ プロジェクトの依存関係。

Gradle は、次の優先順位に従って重複リソースを結合します。

依存関係 → メイン → ビルド フレーバー → ビルドタイプ

たとえば、メインリソースとビルド フレーバーの両方に重複リソースがある場合、Gradle はビルド フレーバー内のリソースを選択します。

同じソースセットに同一のリソースがある場合、Gradle はこれらのリソースを結合できないため、リソース結合エラーを出力します。このエラーは、build.gradle.kts ファイルの sourceSet プロパティで複数のソースセットが定義されている場合に発生する可能性があります(src/main/res/src/main/res2/ の両方に同じリソースが格納されている場合など)。

コードの難読化

難読化の目的は、アプリのクラス、メソッド、フィールドの名前を短くすることで、アプリのサイズを小さくすることにあります。R8 を使用した難読化の例を次に示します。

androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
    android.content.Context mContext -> a
    int mListItemLayout -> O
    int mViewSpacingRight -> l
    android.widget.Button mButtonNeutral -> w
    int mMultiChoiceItemLayout -> M
    boolean mShowTitle -> P
    int mViewSpacingLeft -> j
    int mButtonPanelSideLayout -> K

難読化の場合、アプリからコードは削除されませんが、多数のクラス、メソッド、フィールドをインデックスに登録する DEX ファイルを持つアプリのサイズを大幅に縮小できます。ただし、難読化によりコードのさまざまな部分の名前が変更されるので、スタック トレースの検査などの特定のタスクで追加のツールが必要になります。難読化後のスタック トレースについて詳しくは、難読化されたスタック トレースのデコード方法についてのセクションをご確認ください。

また、アプリのメソッドやクラスに予測可能な命名規則を使用している場合(リフレクションを使用している場合など)、それらのシグネチャをエントリ ポイントとして扱い、保持するコードのカスタマイズ方法で説明したように keep ルールを指定する必要があります。keep ルールは、アプリの最終的な DEX 内でそのコードだけでなく、その元の命名規則も維持するように R8 に指示します。

難読化されたスタック トレースのデコード

R8 でコードを難読化すると、クラス名とメソッド名が変更される可能性があるため、スタック トレースを理解することは(不可能ではないにしても)困難になります。元のスタック トレースを取得するには、スタック トレースの再トレースが必要です。

コードの最適化

アプリのサイズをさらに小さくするために、R8 はコードをより深いレベルで検査して、使用されないコードをさらに削除したり、可能であればコードを書き直して冗長性を減らしたりします。以下に、こうした最適化の例を示します。

  • コードが特定の if / else ステートメントの else {} 分岐を絶対に通らない場合、R8 は else {} 分岐のコードを削除することがあります。
  • あるメソッドをコード内の 1 か所だけで呼び出している場合、R8 はそのメソッドを削除して、呼び出しているその唯一の場所にインライン化することがあります。
  • あるクラスに固有のサブクラスが 1 つだけあり、クラス自体はインスタンス化されないことが R8 で確認された場合(1 つの具象実装クラスでのみ使用される抽象型基本クラスなど)、R8 はその 2 つのクラスを結合して、1 つのクラスをアプリから削除することがあります。
  • 詳しくは、Jake Wharton 氏による R8 の最適化に関するブログ投稿をご覧ください。

R8 に対して、個々の最適化を有効または無効にしたり、最適化の動作を変更したりすることはできません。実際に、R8 はデフォルトの最適化を変更しようとする ProGuard ルール(-optimizations- optimizationpasses など)を無視します。この制限は重要です。なぜなら、R8 は改良が続けられていますが、最適化の標準的な動作を維持することで、デベロッパーが直面する可能性がある問題を Android Studio チームが容易にトラブルシューティングして解決できるようになるからです。

なお、最適化を有効にすると、アプリのスタック トレースが変更されます。たとえば、インライン化によりスタック フレームが削除されます。元のスタック トレースを取得する方法については、再トレースに関するセクションをご覧ください。

より積極的な最適化の有効化

R8 には追加の最適化(「フルモード」と呼ばれます)が含まれており、ProGuard とは異なる動作をします。Android Gradle プラグイン バージョン 8.0.0 以降、これらの最適化はデフォルトで有効になっています。

このような追加の最適化を無効にするには、プロジェクトの gradle.properties ファイルに次の行を追加します。

android.enableR8.fullMode=false

追加の最適化により R8 は ProGuard と動作が異なるため、ProGuard 用に設計されたルールを使用している場合、実行時の問題を回避するために追加の ProGuard ルールを含めることが必要になる場合があります。たとえば、コードで Java Reflection API を介してクラスを参照するとします。「フルモード」を使用しない場合、R8 は、コードが実際にそうしなくても、実行時にそのクラスのオブジェクトを調べて操作するつもりであると想定し、クラスとその静的イニシャライザを自動的に保持します。

ただし、「フルモード」を使用する場合、R8 はこの前提を行わず、コードが実行時にクラスを使用しないことを R8 がアサートした場合、アプリの最終的な DEX からクラスが削除されます。つまり、クラスとその静的イニシャライザを保持するには、ルールファイルに keep ルールを含める必要があります。

R8 の「フルモード」の使用中に問題が発生した場合は、R8 のよくある質問のページで考えられる解決策を確認してください。問題を解決できない場合は、バグを報告してください。

スタック トレースの再トレース

R8 で処理されるコードがさまざまな方法で変更されると、スタック トレースがソースコードと完全には一致しなくなるため、スタック トレースを理解することがより困難になります。デバッグ情報が保持されていない場合の行番号の変更もこれに当てはまることがあります。インライン化やアウトライン化など、最適化が原因となる場合もあります。最も大きい原因は、クラスやメソッドでも名前が変更される難読化です。

元のスタック トレースを復元するには、R8 の retrace コマンドライン ツールを利用します。このツールはコマンドライン ツール パッケージにバンドルされています。

アプリのスタック トレースの再トレースに対応するには、モジュールの proguard-rules.pro ファイルに次のルールを追加して、ビルドに再トレースのための十分な情報が保持されるようにする必要があります。

-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

LineNumberTable 属性は、それぞれの位置がスタック トレースに出力されるようメソッドに位置情報を保持します。SourceFile 属性は、可能性のあるすべてのランタイムで実際に位置情報が出力されるようにします。-renamesourcefileattribute は、スタック トレース内のソースファイル名を単に SourceFile に設定するディレクティブです。マッピング ファイルには元のソースファイルが含まれているため、追跡する際に実際の元のファイル名は必要ありません。

R8 は実行されるたびに mapping.txt ファイルを作成します。このファイルに、スタック トレースを元のスタック トレースにマッピングし戻すために必要な情報が含まれます。Android Studio はこのファイルを <module-name>/build/outputs/mapping/<build-type>/ ディレクトリに保存します。

Google Play でアプリを公開するときに、各バージョンのアプリの mapping.txt ファイルをアップロードできます。Android App Bundle を使用して公開する場合は、このファイルが App Bundle のコンテンツに自動的に含まれます。ユーザーから報告された問題のスタック トレースについて、Google Play が再トレースするので、Google Play Console で問題を調べることができます。詳しくは、クラッシュのスタック トレースの難読化を解除する方法に関するヘルプセンターの記事をご覧ください。

R8 でのトラブルシューティング

このセクションでは、R8 による圧縮、難読化、最適化を有効にした場合のトラブルシューティング戦略について説明します。問題に対する解決策が下記にない場合、R8 FAQ ページProGuard のトラブルシューティング ガイドもご覧ください。

削除(または保持)されたコードのレポートの生成

R8 の特定の問題をトラブルシューティングする際に、R8 によってアプリから削除されたすべてのコードのレポートを確認すると役に立つ場合があります。このレポートを生成するモジュールごとに、-printusage <output-dir>/usage.txt をカスタム ルールファイルに追加してください。R8 を有効にしてアプリをビルドすると、指定したファイル名のレポートが指定したパスに R8 から出力されます。削除されたコードのレポートは次のような形式です。

androidx.drawerlayout.R$attr
androidx.vectordrawable.R
androidx.appcompat.app.AppCompatDelegateImpl
    public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
    public boolean hasWindowFeature(int)
    public void setHandleNativeActionModesEnabled(boolean)
    android.view.ViewGroup getSubDecor()
    public void setLocalNightMode(int)
    final androidx.appcompat.app.AppCompatDelegateImpl$AutoNightModeManager getAutoNightModeManager()
    public final androidx.appcompat.app.ActionBarDrawerToggle$Delegate getDrawerToggleDelegate()
    private static final boolean DEBUG
    private static final java.lang.String KEY_LOCAL_NIGHT_MODE
    static final java.lang.String EXCEPTION_HANDLER_MESSAGE_SUFFIX
...

プロジェクトの keep ルールから R8 によって特定されたエントリ ポイントのレポートを表示する場合は、カスタム ルールファイルに -printseeds <output-dir>/seeds.txt を含めてください。R8 を有効にしてアプリをビルドすると、指定したファイル名のレポートが指定したパスに R8 から出力されます。保持されたエントリ ポイントのレポートは次のような形式になります。

com.example.myapplication.MainActivity
androidx.appcompat.R$layout: int abc_action_menu_item_layout
androidx.appcompat.R$attr: int activityChooserViewStyle
androidx.appcompat.R$styleable: int MenuItem_android_id
androidx.appcompat.R$styleable: int[] CoordinatorLayout_Layout
androidx.lifecycle.FullLifecycleObserverAdapter
...

リソース圧縮のトラブルシューティング

リソースを圧縮すると、アプリから削除されたリソースの概要が [Build] ウィンドウに表示されます(最初にウィンドウの左側にあるビューの切り替えアイコン をクリックして、Gradle からの詳細なテキスト出力を表示する必要があります)。次に例を示します。

:android:shrinkDebugResources
Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning

また、Gradle は <module-name>/build/outputs/mapping/release/(ProGuard の出力ファイルと同じフォルダ)に resources.txt という名前の診断ファイルを作成します。このファイルには、どのリソースが他のリソースを参照したか、どのリソースが使用または削除されたかなどの詳細情報が記載されます。

たとえば、アプリに @drawable/ic_plus_anim_016 が引き続き存在している理由を特定するには、resources.txt ファイルを開き、そのファイル名を検索します。次の例のように、そのファイルが別のリソースから参照されていることが確認できる場合があります。

16:25:48.005 [QUIET] [system.out] &#64;drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]     &#64;drawable/ic_plus_anim_016

ここで、@drawable/add_schedule_fab_icon_anim が到達可能である理由を確認するため、上に向かって調べていくと、そのリソースが「The root reachable resources are:」の下に記載されていることがわかります。つまり、add_schedule_fab_icon_anim に対するコード参照があります(到達可能なコード内にその R.drawable ID があります)。

厳密なチェックを使用していない場合、動的に読み込まれるリソースの名前の作成に使用されそうな文字列定数があると、リソース ID が到達可能であるとマークされることがあります。この場合、ビルド出力でそのリソース名を探すと、次のようなメッセージが見つかることがあります。

10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
    used because it format-string matches string pool constant ic_plus_anim_%1$d.

このような文字列のいずれかを確認して、特定のリソースの動的な読み込みにその文字列が使用されないと確信した場合、保持するリソースのカスタマイズ方法での説明のとおり、tools:discard 属性を使用して、そのリソースを削除するようビルドシステムに指示できます。