64K を超えるメソッドを使用するアプリ向けに Multidex を有効化する

アプリの API の minSdk が 20 以下で、アプリおよび参照するライブラリが 65,536 メソッドを超えると、次のようなビルドエラーが発生し、Android ビルド アーキテクチャの制限に達したことが示されます。

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

以前のバージョンのビルドシステムでは、異なるエラーが報告されますが、問題の内容は同じです。

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536

どちらのエラー状態でも「65536」という数字が表示されます。これは、単一の Dalvik Executable(DEX)バイトコード ファイル内でコードによって呼び出し可能な参照の総数を示しています。このページでは、「multidex」というアプリ設定を有効にして、複数の DEX ファイルのビルドと読み取りを可能にすることで、上記の制限を回避する方法について説明します。

64K 参照制限について

Android アプリ(APK)ファイルには、Dalvik Executable(DEX)形式のバイトコード実行ファイルが含まれており、その中にアプリの実行時に使用されるコンパイル済みコードが格納されます。Dalvik Executable の仕様により、単一の DEX ファイル内で参照できるメソッドの総数は 65,536 に制限されています。対象となるメソッドは、Android フレームワーク メソッドや、ライブラリ メソッド、独自コード内のメソッドなどです。

コンピュータ サイエンスにおいては、Kilo や K は 1,024(2^10)を示します。65,536 は 64×1,024 に等しいため、この制限は「64K 参照制限」と呼ばれます。

Android 5.0 よりも前の Multidex サポート

Android 5.0(API レベル 21)よりも前のプラットフォーム バージョンでは、アプリコードを実行するために Dalvik ランタイムを使用します。デフォルトでは、Dalvik はアプリに対し、APK ごとに 1 つの classes.dex バイトコード ファイルという制限を設けています。モジュール レベルの build.gradle ファイルまたは build.gradle.kts ファイルに multidex ライブラリを追加すると、この制限を回避できます。

Groovy

dependencies {
    def multidex_version = "2.0.1"
    implementation "androidx.multidex:multidex:$multidex_version"
}

Kotlin

dependencies {
    val multidex_version = "2.0.1"
    implementation("androidx.multidex:multidex:$multidex_version")
}

このライブラリは、アプリのプライマリ DEX ファイルの一部となり、追加の DEX ファイルとそれに含まれるコードに対するアクセスを管理します。 このライブラリの現在のバージョンを確認するには、バージョンのページで Multidex に関する情報をご覧ください。

詳しくは、以下の Multidex 向けにアプリを設定するをご覧ください。

Android 5.0 以降の Multidex サポート

Android 5.0(API レベル 21)以降は、ART と呼ばれるランタイムを使用しており、APK ファイルから複数の DEX ファイルを読み込む機能がネイティブでサポートされています。ART は、アプリのインストール時にプリコンパイルを実行して classesN.dex ファイルをスキャンし、それらのファイルを単一の OAT ファイルにコンパイルして Android デバイス上で実行できるようにします。したがって、minSdkVersion が 21 以上の場合は Multidex がデフォルトで有効になり、multidex ライブラリは必要ありません。

Android 5.0 ランタイムについて詳しくは、Android ランタイム(ART)と Dalvik をご覧ください。

注: Android Studio を使用してアプリを実行する場合、ビルドはデプロイ先のターゲット デバイス向けに最適化されます。ターゲット デバイスが Android 5.0 以降を実行している場合は、Multidex の有効化も含まれます。この最適化が適用されるのは Android Studio を使用したアプリのデプロイに限られるため、64K 制限を回避するには Multidex のリリースビルドの構成が必要になる場合があります。

64K 制限を回避する

64K 以上のメソッド参照を使用できるようにアプリを設定する前に、アプリコードまたは含まれているライブラリで定義されたメソッドを含め、アプリコードによって呼び出される参照の総数を減らすための対策を講じます。

DEX 参照制限への到達を回避するための方策を以下に示します。

アプリの直接的および推移的な依存関係を見直す
アプリに大きなライブラリ依存関係を含める方が、アプリにコードを追加するよりも価値が高いかどうかを検討します。よくある問題となるパターンは、有用なユーティリティ メソッドがいくつかあるというだけで、大きなライブラリを含めてしまうことです。多くの場合、アプリコードの依存関係を削減することで、DEX 参照制限を回避できます。
R8 を使用して未使用のコードを削除する
コードの圧縮を有効にし、リリースビルドに対して R8 を実行します。圧縮を有効にすると、未使用のコードが APK から除外されるようになります。コード圧縮が正しく設定されていれば、未使用のコードとリソースを依存関係から削除することもできます。

このようなテクニックを活用することで、アプリ内で Multidex を有効化せずに済み、さらには APK 全体のサイズも削減できます。

Multidex 向けにアプリを設定する

注: minSdkVersion が 21 以上に設定されている場合は Multidex はデフォルトで有効になり、multidex ライブラリは必要ありません。

一方、minSdkVersion が 20 以下に設定されている場合は、multidex ライブラリを使用し、アプリ プロジェクトに以下のような変更を加える必要があります。

  1. 次に示すように、モジュール レベルの build.gradle ファイルを編集して Multidex を有効にし、multidex ライブラリを依存関係として追加します。

    Groovy

    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 33
            multiDexEnabled true
        }
        ...
    }
    
    dependencies {
        implementation "androidx.multidex:multidex:2.0.1"
    }
    

    Kotlin

    android {
        defaultConfig {
            ...
            minSdk = 15 
            targetSdk = 33
            multiDexEnabled = true
        }
        ...
    }
    
    dependencies {
        implementation("androidx.multidex:multidex:2.0.1")
    }
    
  2. Application クラスのオーバーライドの状況に応じて、以下のいずれかを実施します。
    • Application クラスをオーバーライドしない場合は、次のようにマニフェスト ファイルを編集し、<application> タグ内で android:name を設定します。

      <?xml version="1.0" encoding="utf-8"?>
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.myapp">
          <application
                  android:name="androidx.multidex.MultiDexApplication" >
              ...
          </application>
      </manifest>
      
    • Application クラスをオーバーライドする場合は、次のように MultiDexApplication を拡張します。

      Kotlin

      class MyApplication : MultiDexApplication() {...}
      

      Java

      public class MyApplication extends MultiDexApplication { ... }
      
    • Application クラスをオーバーライドしているが、基本クラスを変更できない場合は、代わりに attachBaseContext() メソッドをオーバーライドし、MultiDex.install(this) を呼び出すことにより、Multidex を有効にできます。

      Kotlin

      class MyApplication : SomeOtherApplication() {
      
          override fun attachBaseContext(base: Context) {
              super.attachBaseContext(base)
              MultiDex.install(this)
          }
      }
      

      Java

      public class MyApplication extends SomeOtherApplication {
        @Override
        protected void attachBaseContext(Context base) {
           super.attachBaseContext(base);
           MultiDex.install(this);
        }
      }
      

      注意: MultiDex.install() が完了する前に MultiDex.install() またはその他のコードをリフレクションまたは JNI を通じて実行しないようにしてください。Multidex トレースはそのような呼び出しを追跡しないため、ClassNotFoundException や、DEX ファイル間の不正なクラス パーティションに起因する検証エラーが発生します。

これで、アプリのビルド時に、Android ビルドツールによってプライマリ DEX ファイル(classes.dex)が作成され、必要に応じて補助的な DEX ファイル(classes2.dexclasses3.dex など)が作成されるようになります。そして、ビルドシステムによって、すべての DEX ファイルが APK にパッケージ化されます。

実行時に、multidex API は特別なクラスローダーを使用して、メインの classes.dex ファイルだけでなく、利用可能なすべての DEX ファイルでメソッドを検索します。

multidex ライブラリの制限

multidex ライブラリには既知の制限事項がいくつかあります。 アプリのビルド構成に multidex ライブラリを組み込む際は、次の点を考慮してください。

  • 起動時に DEX ファイルをデバイスのデータ パーティションにインストールする処理は複雑であるため、セカンダリ DEX ファイルが大きいと、ANR(アプリケーション応答なし)エラーが発生する可能性があります。この問題を回避するには、コード圧縮を有効にして、DEX ファイルのサイズを最小限に抑え、コードの未使用部分を削除する必要があります。
  • Android 5.0(API レベル 21)よりも前のバージョンの場合、Multidex を使用するだけでは linearalloc 制限を回避できません(問題 37008143)。Android 4.0(API レベル 14)で上限が引き上げられましたが、完全には解決したわけではありません。

    Android 4.0 より前のバージョンでは、DEX インデックス制限に到達する前に linearalloc 制限に到達することがあります。API レベル 14 よりも前の API レベルをターゲットにしている場合、アプリの起動時や特定のクラスグループの読み込み時に問題が発生する可能性があるため、対象のプラットフォーム バージョンで徹底的にテストを実施しておく必要があります。

    コードを圧縮すると、このような問題が減り、場合によっては完全に解消できることがあります。

プライマリ DEX ファイル内に必要なクラスを宣言する

multidex アプリ用に各 DEX ファイルを作成する際、ビルドツールは、複雑な判断基準に基づいて、アプリを正常に起動するうえでプライマリ DEX ファイル内に必要となるクラスを決定します。起動時に必要なクラスが 1 つでもプライマリ DEX ファイル内に含まれていないと、アプリは java.lang.NoClassDefFoundError エラーでクラッシュします。

アプリコードから直接アクセスされるコードの場合は、ビルドツールがそのコードパスを認識します。他方、使用するライブラリに複雑な依存関係がある場合など、コードパスの可視性が低い場合には、エラーが発生する可能性があります。たとえば、コードがイントロスペクションを使用している場合や、ネイティブ コードから Java メソッド呼び出しを使用している場合、プライマリ DEX ファイル内に必要なクラスとして認識されない場合があります。

java.lang.NoClassDefFoundError を受信した場合には、ビルドタイプの multiDexKeepProguard プロパティで追加クラスを宣言し、プライマリ DEX ファイル内に必要なクラスとして手動で指定する必要があります。クラスが multiDexKeepProguard ファイル内で合致すると、プライマリ DEX ファイルに追加されます。

multiDexKeepProguard プロパティ

multiDexKeepProguard ファイルは、ProGuard と同じ形式を使用し、ProGuard 文法全体をサポートします。アプリに保持するコードをカスタマイズする方法について詳しくは、保持するコードをカスタマイズするをご覧ください。

multiDexKeepProguard 内で指定するファイルには、有効な ProGuard 構文で -keep オプションを含める必要があります。(例: -keep com.example.MyClass.class)。次のような multidex-config.pro という名前のファイルを作成できます。

-keep class com.example.MyClass
-keep class com.example.MyClassToo

パッケージ内のすべてのクラスを指定する場合、ファイルは次のようになります。

-keep class com.example.** { *; } // All classes in the com.example package

次に、このファイルのビルドタイプを宣言します。

Groovy

android {
    buildTypes {
        release {
            multiDexKeepProguard file('multidex-config.pro')
            ...
        }
    }
}

Kotlin

android {
    buildTypes {
        getByName("release") {
            multiDexKeepProguard = file("multidex-config.pro")
            ...
        }
    }
}

開発ビルド内で Multidex を最適化する

multidex 設定の場合、ビルドシステムが複雑な判断を行い、プライマリ DEX ファイル内に必要なクラスや、セカンダリ DEX ファイル内に含めることのできるクラスを判別する必要があるため、ビルド処理に非常に長い時間がかかります。そのため、Multidex を使用した増分ビルドも時間がかかることが多く、開発プロセスの遅延につながる可能性があります。

増分ビルドに必要とされる時間を削減するには、「pre-dex」を使用して、ビルド間で Multidex 出力を再利用します。 pre-dex は、Android 5.0(API レベル 21)以降に限り利用可能な ART 形式を使用します。Android Studio を使用している場合、IDE は、Android 5.0(API レベル 21)以降を実行しているデバイスにアプリをデプロイする際に、自動的に pre-dex を使用します。 なお、Gradle ビルドをコマンドラインから実行する場合は、minSdkVersion を 21 以上に設定して、pre-dex を有効にする必要があります。

製品版ビルドの設定を維持するには、プロダクト フレーバーを使用して、2 つのアプリ バージョンを作成します。1 つは開発フレーバー、もう 1 つはリリース フレーバーで、次のようにそれぞれの minSdkVersion を異なる値にします。

Groovy

android {
    defaultConfig {
        ...
        multiDexEnabled true
        // The default minimum API level you want to support.
        minSdkVersion 15
    }
    productFlavors {
        // Includes settings you want to keep only while developing your app.
        dev {
            // Enables pre-dexing for command-line builds. When using
            // Android Studio 2.3 or higher, the IDE enables pre-dexing
            // when deploying your app to a device running Android 5.0
            // (API level 21) or higher, regardless of minSdkVersion.
            minSdkVersion 21
        }
        prod {
            // If you've configured the defaultConfig block for the production version of
            // your app, you can leave this block empty and Gradle uses configurations in
            // the defaultConfig block instead. You still need to include this flavor.
            // Otherwise, all variants use the "dev" flavor configurations.
        }
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                                 'proguard-rules.pro'
        }
    }
}
dependencies {
    implementation "androidx.multidex:multidex:2.0.1"
}

Kotlin

android {
    defaultConfig {
        ...
        multiDexEnabled = true
        // The default minimum API level you want to support.
        minSdk = 15
    }
    productFlavors {
        // Includes settings you want to keep only while developing your app.
        create("dev") {
            // Enables pre-dexing for command-line builds. When using
            // Android Studio 2.3 or higher, the IDE enables pre-dexing
            // when deploying your app to a device running Android 5.0
            // (API level 21) or higher, regardless of minSdkVersion.
            minSdk = 21
        }
        create("prod") {
            // If you've configured the defaultConfig block for the production version of
            // your app, you can leave this block empty and Gradle uses configurations in
            // the defaultConfig block instead. You still need to include this flavor.
            // Otherwise, all variants use the "dev" flavor configurations.
        }
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android.txt"),
                                                 "proguard-rules.pro")
        }
    }
}

dependencies {
    implementation("androidx.multidex:multidex:2.0.1")
}

Android Studio やコマンドラインを使用した場合のビルド速度を向上させるためのヒントについては、ビルド速度を最適化するをご覧ください。 ビルド バリアントの利用方法については、ビルド バリアントを設定するをご覧ください。

ヒント: 異なる Multidex のニーズに応じてビルド バリアントが異なる場合は、バリアントごとに異なるマニフェスト ファイルを設定することもできます(API レベル 20 以下のファイルに限り、<application> タグ名が変更されます)。または、バリアントごとに異なる Application サブクラスを作成することもできます(API レベル 20 以下のサブクラスに限り、MultiDexApplication クラスが拡張されるか、MultiDex.install(this) が呼び出されます)。

Multidex アプリをテストする

MonitoringInstrumentation インストゥルメンテーション(または AndroidJUnitRunner インストゥルメンテーション)を使用している場合、Multidex アプリ用のインストゥルメンテーション テストを記述する際に追加の設定は不要です。別の Instrumentation を使用する場合は、その onCreate() メソッドを次のコードでオーバーライドする必要があります。

Kotlin

fun onCreate(arguments: Bundle) {
  MultiDex.install(targetContext)
  super.onCreate(arguments)
  ...
}

Java

public void onCreate(Bundle arguments) {
  MultiDex.install(getTargetContext());
  super.onCreate(arguments);
  ...
}