Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

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

アプリ、およびアプリの参照するライブラリが 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 バイトコード ファイルという制限を設けています。プロジェクトに multidex サポート ライブラリを追加すると、この制限を回避できます。

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

このライブラリの現在のバージョンを確認するには、バージョンのページで Multidex に関する情報をご覧ください。

AndroidX を使用していない場合は、代わりに以下のサポート ライブラリ依存関係を追加してください。

    dependencies {
      implementation 'com.android.support:multidex:1.0.3'
    }
    

このライブラリは、アプリのプライマリ DEX ファイルの一部となり、追加の DEX ファイルとそこに含まれるコードに対するアクセスを管理することができます。詳しくは、以下の multidex 向けにアプリを設定するをご覧ください。

Android 5.0 以降の multidex サポート

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

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

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

64K 制限を回避する

64K 以上のメソッド参照が有効になるようにアプリを設定する前に、アプリのコードや含まれているライブラリによって定義されているメソッドなど、アプリコードが呼び出す参照の総数を削減するよう取り組む必要があります。DEX 参照制限の到達を回避するための戦略を以下に示します。

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

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

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

minSdkVersion が 21 以上の場合は multidex がデフォルトで有効であり、multidex サポート ライブラリは必要ありません。

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

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

        android {
            defaultConfig {
                ...
                minSdkVersion 15
                targetSdkVersion 28
                multiDexEnabled true
            }
            ...
        }
    
        dependencies {
          implementation 'com.android.support:multidex:1.0.3'
        }
        
  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="android.support.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(アプリケーション応答なし)エラーが発生する可能性があります。この場合、ProGuard を使用したコード圧縮を適用して、DEX ファイルのサイズを最小限に抑え、コードの未使用部分を削除する必要があります。
  • Android 5.0(API レベル 21)よりも前のバージョンの場合、multidex を使用するだけでは linearalloc 制限を回避できません(問題 78035)。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 を受信した場合には、ビルドタイプの multiDexKeepFile プロパティまたは multiDexKeepProguard プロパティで宣言することで、このような追加クラスをプライマリ DEX ファイル内に必要なクラスとして手動で指定する必要があります。クラスが multiDexKeepFile ファイル内または multiDexKeepProguard ファイル内で合致すると、プライマリ DEX ファイルに追加されます。

multiDexKeepFile プロパティ

multiDexKeepFile 内で指定するファイルは、com/example/MyClass.class 形式で 1 行ごとに 1 つのクラスを格納している必要があります。たとえば、次のような multidex-config.txt と呼ばれるファイルを作成できます。

    com/example/MyClass.class
    com/example/MyOtherClass.class
    

次に、このファイルのビルドタイプを以下のように宣言できます。

    android {
        buildTypes {
            release {
                multiDexKeepFile file('multidex-config.txt')
                ...
            }
        }
    }
    

Gradle は、build.gradle ファイルからの相対パスを読み取ります。そのため、上記のサンプルコードは、multidex-config.txtbuild.gradle ファイルと同じディレクトリにある場合に限り機能します。

multiDexKeepProguard プロパティ

multiDexKeepProguard ファイルは ProGuard と同じ形式を使用し、ProGuard の文法をすべてサポートします。ProGuard の形式と文法の詳細については、ProGuard マニュアルの keep オプションをご覧ください。

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
    

次に、このファイルのビルドタイプを以下のように宣言できます。

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

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

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

増分ビルドに必要とされる時間を削減するには、「pre-dex」を使用して、ビルド間で multidex 出力を再利用する必要があります。pre-dex は、Android 5.0(API レベル 21)以降に限り利用可能な ART 形式を使用します。Android Studio 2.3 以降を使用している場合、IDE は、Android 5.0(API レベル 21)以降を実行しているデバイスにアプリをデプロイする際に、自動的にこの機能を使用します。

ヒント: Android Plugin for Gradle 3.0.0 以降の場合、クラス単位の dex など、ビルド速度を最適化するためのさまざまな改善点が組み込まれています(そのため、編集したクラスだけに re-dex が行われます)。一般に、最適な開発環境を構築するには、常に最新バージョンの Android Studio と Android プラグインにアップグレードする必要があります。

なお、Gradle ビルドをコマンドラインから実行する場合は、minSdkVersion を 21 以上に設定して、pre-dex を有効にする必要があります。実稼働ビルドの設定を保持するには、プロダクト フレーバーを使用して 2 つのアプリ バージョンを作成することをおすすめします。1 つは開発フレーバー、もう 1 つはリリース フレーバーで、次のように minSdkVersion の値をそれぞれ異なる値にします。

    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 what you set for
                // 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 'com.android.support:multidex:1.0.3'
    }
    

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);
      ...
    }