Как включить поддержку 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 (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 (ART) и Dalvik…

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

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

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

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

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

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

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

Примечание. Если для параметра minSdkVersion задано значение 21 или выше, многофайловый режим включен по умолчанию и библиотека 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 обычно занимают больше времени и могут замедлить процесс разработки.

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

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

При написании инструментальных тестов для приложений с поддержкой multidex дополнительная настройка не требуется, если вы используете инструментарий MonitoringInstrumentation или AndroidJUnitRunner. Если вы используете другой класс 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);
  ...
}