Как включить поддержку multidex для приложений с более чем 64 000 методов

Если в вашем приложении используется minSdk API 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.

Ограничение на количество ссылок (64 000)

Файлы APK содержат исполняемый байт-код в виде файлов Dalvik Executable (DEX), в которых находится скомпилированный код, используемый для запуска приложения для Android. Спецификация Dalvik Executable ограничивает общее количество методов, на которые можно ссылаться в одном файле DEX, до 65 536, включая методы фреймворка Android, методы библиотеки и методы в вашем собственном коде.

В компьютерных науках кило, или К, означает 1024 (или 2^10). Поскольку 65 536 равно 64 x 1024, это ограничение называется _ограничением на 64 тыс. ссылок_.

Поддержка Multidex в версиях до Android 5.0

В версиях платформы до Android 5.0 (уровень API 21) для выполнения кода приложения используется среда выполнения Dalvik. По умолчанию Dalvik ограничивает приложения одним файлом байт-кода classes.dex на APK. Чтобы обойти это ограничение, добавьте библиотеку multidex в файл build.gradle или build.gradle.kts на уровне модуля:

Яркий

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…

Поддержка Multidex для Android 5.0 и более поздних версий

В Android 5.0 (уровень API 21) и более поздних версиях используется среда выполнения ART, которая поддерживает загрузку нескольких DEX-файлов из APK-файлов. ART выполняет предварительную компиляцию во время установки приложения, сканируя файлы classesN.dex и компилируя их в один файл OAT для выполнения на устройстве Android. Поэтому если у вас minSdkVersion версии 21 или выше, то multidex включен по умолчанию и библиотека multidex не нужна.

Дополнительную информацию о среде выполнения Android 5.0 можно найти в статье Android Runtime (ART) и Dalvik.

Примечание. При запуске приложения с помощью Android Studio сборка оптимизируется для целевых устройств, на которых вы его развертываете. В частности, это позволяет включить multidex, если целевые устройства работают под управлением Android 5.0 и более поздних версий. Поскольку эта оптимизация применяется только при развертывании приложения с помощью Android Studio, вам все равно может потребоваться настроить конечную сборку для multidex, чтобы избежать ограничения в 64 тыс. методов.

Как избежать ограничения в 64 000 методов

Прежде чем настраивать приложение для использования 64 000 или более ссылок на методы, уменьшите общее количество ссылок, вызываемых кодом приложения, включая методы, определенные кодом приложения или включенными библиотеками.

Чтобы не превысить лимит ссылок DEX, следуйте приведенным ниже рекомендациям.

Как проверить прямые и транзитивные зависимости приложения
Подумайте, стоит ли добавлять в приложение большую библиотеку, если вам нужно лишь несколько ее методов. Чтобы не превысить лимит ссылок DEX, сократите зависимости кода приложения.
Удаление неиспользуемого кода с помощью R8
Включите сжатие кода, чтобы запустить R8 для конечных сборок. Включите сжатие, чтобы в APK не было неиспользуемого кода. Если сжатие кода настроено правильно, оно также может удалить неиспользуемый код и ресурсы из ваших зависимостей.

Эти методы помогут вам уменьшить общий размер APK и избежать необходимости использовать multidex в приложении.

Как настроить приложение для multidex

Примечание. Если для параметра minSdkVersion задано значение 21 или выше, multidex включен по умолчанию и библиотека multidex не требуется.

Если в параметре minSdkVersion указано значение 20 или ниже, то вам необходимо использовать библиотеку multidex и внести следующие изменения в проект приложения:

  1. Измените файл build.gradle на уровне модуля, чтобы включить multidex и добавить библиотеку multidex в качестве зависимости, как показано ниже.

    Яркий

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

    Kotlin

    android {
        defaultConfig {
            ...
            minSdk = 15 
            targetSdk = 36
            multiDexEnabled = true
        }
        ...
    }
    
    dependencies {
        implementation("androidx.multidex:multidex:2.0.1")
    }
  2. В зависимости от того, переопределяете ли вы класс Application, выполните одно из следующих действий:
    • Если вы не переопределяете класс Application, измените файл манифеста, чтобы задать android:name в теге <application> следующим образом:

      <?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() или любой другой код через рефлексию или JNI до завершения MultiDex.install(). Трассировка Multidex не будет отслеживать эти вызовы, что приведет к ошибкам ClassNotFoundException или ошибкам проверки из-за неправильного разделения классов между файлами DEX.

Теперь при сборке приложения инструменты сборки Android создают основной DEX-файл (classes.dex) и вспомогательные DEX-файлы (classes2.dex, classes3.dex и т. д.) по мере необходимости. Затем система сборки упаковывает все DEX-файлы в APK.

Во время выполнения вместо поиска только в основном файле classes.dex API multidex используют специальный загрузчик классов для поиска всех доступных файлов DEX для ваших методов.

Ограничения библиотеки multidex

У библиотеки multidex есть некоторые ограничения. При добавлении библиотеки в конфигурацию сборки приложения учитывайте следующее:

  • Установка DEX-файлов в раздел данных устройства во время запуска – сложный процесс, который может привести к ошибкам ANR ("Приложение не отвечает"), если дополнительные DEX-файлы имеют большой размер. Чтобы избежать этой проблемы, включите сокращение кода. Это позволит уменьшить размер DEX-файлов и удалить неиспользуемые части кода.
  • При работе с версиями до Android 5.0 (уровень API 21) использования multidex недостаточно, чтобы обойти ограничение linearalloc (проблема 37008143). В Android 4.0 (уровень API 14) это ограничение было увеличено, но проблема не была решена полностью.

    В версиях ниже Android 4.0 вы можете достичь лимита linearalloc до того, как достигнете лимита индекса DEX. Если вы используете целевой уровень API ниже 14, тщательно протестируйте приложение на этих версиях платформы, поскольку при запуске или загрузке определенных групп классов могут возникнуть проблемы.

    Сокращение кода может уменьшить или даже устранить эти проблемы.

Объявите классы, необходимые в основном DEX-файле

При создании каждого файла DEX для приложения multidex инструменты сборки выполняют сложные вычисления, чтобы определить, какие классы необходимы в основном файле DEX для успешного запуска приложения. Если в основном DEX-файле отсутствует класс, необходимый для запуска, приложение завершит работу с ошибкой java.lang.NoClassDefFoundError.

Инструменты сборки распознают пути выполнения для кода, к которому осуществляется прямой доступ из кода приложения. Однако эта проблема может возникнуть, когда пути выполнения менее заметны, например когда используемая вами библиотека имеет сложные зависимости. Например, если в коде используется интроспекция или вызов методов Java из нативного кода, то такие классы могут не распознаваться как необходимые в основном DEX-файле.

Если вы получаете ошибку java.lang.NoClassDefFoundError, вам нужно вручную указать дополнительные классы, необходимые в основном DEX-файле, объявив их с помощью свойства multiDexKeepProguard в типе сборки. Если класс совпадает с указанным в файле multiDexKeepProguard, он добавляется в основной DEX-файл.

Свойство multiDexKeepProguard

Файл multiDexKeepProguard использует тот же формат, что и ProGuard, и поддерживает всю грамматику ProGuard. Подробнее о том, как настроить сохранение кода…

Файл, указанный в multiDexKeepProguard, должен содержать параметры -keep в любом допустимом синтаксисе ProGuard. например -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')
            ...
        }
    }
}

Kotlin

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

Оптимизируйте multidex в сборках для разработки

Конфигурация multidex значительно увеличивает время сборки, поскольку системе приходится принимать сложные решения о том, какие классы должны быть включены в основной DEX-файл, а какие – в дополнительные. Это означает, что инкрементные сборки с использованием multidex обычно занимают больше времени и могут замедлить процесс разработки.

Чтобы сократить время инкрементной сборки, используйте предварительную декомпиляцию. Это позволит повторно использовать выходные данные multidex между сборками. Предварительная декомпиляция основана на формате ART, который доступен только в Android 5.0 (уровень API 21) и более поздних версиях. Если вы используете Android Studio, IDE автоматически выполняет предварительную декомпиляцию при развертывании приложения на устройстве с Android 5.0 (уровень API 21) или более поздней версии. Однако если вы запускаете сборки Gradle из командной строки, то для включения предварительной декомпиляции необходимо установить для minSdkVersion значение 21 или выше.

Чтобы сохранить настройки для рабочей сборки, вы можете создать две версии приложения, используя версии продукта: одну для разработки, а другую для выпуска. При этом значения 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 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, то для приложений с несколькими DEX-файлами дополнительная настройка не требуется. Если вы используете другой класс 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);
  ...
}