Решение проблемы 64k MultiDex в Dalvik.

Vitaly Veldyasov
5 min readApr 17, 2017

Перевод статьи Miroslav Stanek (http://frogermcs.github.io/MultiDex-solution-for-64k-limit-in-Dalvik/)

Почти каждый разработчик Android знает грустную правду о Dalvik, java виртуальной машины, которая используется приложениями и некоторыми системными сервисами, и имеет одно большое ограничение — один .dex файл может иметь только 64К (если быть точнее 65536) методов.

Что это означает (для тех кто еще не сталкивался с этим ограничением)? Коротко, если ваше приложение включает много методов, и вы вызываете один из них, который расположен после 65536 позиции, то ваше приложение завершится с ошибкой:

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

В статье “DEX Sky’s the limit? No, 65K methods is” вы можете найти больше деталей и объяснений по этой проблеме.

64k это большое количество. Мне действительно нужно позаботиться об этом лимите?

Да, нужно. Android развивается очень быстро. Также со стороны разработчиков. Библиотеки развиваются, Google выпускает новые Play Services в которой каждая новая версия имеет несколько сотен или тысяч новых методов.

Чтобы показать проблему, я создал небольшой проект.

Предположим мы хотим создать MVP проект с:

  • Simple REST (json) клиентом
  • Будем использовать clean project structure and good programming practises
  • Некоторые простые и прикольные анимации и современные UI элементы
  • Логин через facebook
  • Совместимость с версиями Android 4 и 5.

Достижение предела

Ок, давайте создадим простой проект в Android Studio с пустым Activity и #minSdkVersion=”15” (во время написания этого поста я использовал Android Studio ver.0.8.14 и Android Gradle plugin ver.0.13).

Теперь я добавлю свои любимые библиотеки. Ниже полный список зависимостей нашего проекта из<project>/app/build.gradle:

dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:support-v4:21.+' compile 'com.android.support:support-v13:21.+' compile 'com.android.support:appcompat-v7:21.0.+' compile 'com.android.support:palette-v7:+' compile 'com.android.support:recyclerview-v7:+' compile 'com.google.android.gms:play-services:6.1.+' compile 'com.google.guava:guava:18.0' compile 'com.google.code.gson:gson:2.3' compile 'com.netflix.rxjava:rxjava-android:0.20.5' provided 'com.squareup.dagger:dagger-compiler:1.2.2' compile 'com.squareup.retrofit:retrofit:1.7.0' compile 'com.squareup.dagger:dagger:1.2.2' compile 'com.squareup.picasso:picasso:2.3.4' compile 'com.squareup:otto:1.3.5' compile 'com.jakewharton:butterknife:5.1.2' compile 'com.jakewharton.timber:timber:2.4.0' compile 'com.newrelic.agent.android:android-agent:3.+' compile('com.crashlytics.sdk.android:crashlytics:2.0.0@aar') { transitive = true; } compile 'se.emilsjolander:stickylistheaders:2.5.1' compile 'com.astuetz:pagerslidingtabstrip:1.0.1' compile 'com.facebook.rebound:rebound:0.3.6' }

Что мы имеем? Коротко:

  • Play Services и support libraries
  • Dagger для dependency injection и clean project architecture
  • Некоторые инструменты для быстрого и прикольного программирования (Guava, Butterknife, Timber)
  • Некоторые UI/Animation инструменты (Rebound — супер инструменты анимации от facebook, RecyclerView, Palette, SlidingTabStrip и тд.)
  • Retrofit, Gson, Picasso для работы с сетью
  • RxJava для асинхронных тасков
  • NewRelic и Crashlytics краш репортов и статистики

Вы можете посмотреть этот проект здесь. Также еще один с facebook SDK.

Отлично, теперь давайте начнем с используемых методов (да, не забываем, что 64k включает также методы из всех наших зависимостей в проекте).

Для начала сбилдим и запустим проект (у нас появится .apk или/и .dex файлы. Вы можете сделать это по нажатию на ▶️ в Android Studio или через запуск команды:

$ ./gradlew assembleDebug

в каталоге проекта.

Наш .apk файл должен располагаться в <project>/app/builds/outputs/apk/app-debug.apk.

Теперь проанализируем его. Для этого я использовал инструмент dex-method-counts . После его скачивания мы должны запустить две команды (скопировано из README):

$ ./gradlew assemble $ ./dex-method-counts path/to/App.apk # or .zip or .dex or directory

Результаты

Результат удивил меня, потому что я только что обновил Google Play Services с версии 5 до 6 и… вобщем, лучше посмотрите сами:

Read in 63897 method IDs. 
<root>: 63897
android: 14275
support: 11454
...
butterknife: 161
com: 40866
crashlytics: 631
...
facebook: 221
...
google: 36603
android: 21839
...
newrelic: 2579
...
squareup: 511
okhttp: 8
otto: 57
picasso: 446
io: 1125
fabric: 1102
...
github: 23
froger: 23
hellomultidex: 23
java: 1928
retrofit: 474
rx: 3337
timber: 65
log: 65
Overall method count: 63897

Такие дела. Мы не написали еще ни строчки кода, но уже почти достигли dex предела с нашими 63897 методами.

Да, чуть не забыл. Мы не подключили Analytics библиотеку. Так, давайте добавим FlurryAnalytics. И также добавим Parse SDK.

Сделаем билд еще раз и … 💥бадыщь 💥:

UNEXPECTED TOP-LEVEL EXCEPTION: com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536  at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:502)  at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:283)  at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:491)  at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:168)  at com.android.dx.merge.DexMerger.merge(DexMerger.java:189)  at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:454)  at com.android.dx.command.dexer.Main.runMonoDex(Main.java:302)  at com.android.dx.command.dexer.Main.run(Main.java:245)  at com.android.dx.command.dexer.Main.main(Main.java:214)  at com.android.dx.command.Main.main(Main.java:106)

Да, это случилось. И мы по прежнему не написали ни строчки кода.

Лечение

Конечно наиболее правильное решение это ProGuard. Но еще раз — мы работаем по MVP и не хотим иметь дело с:

FATAL EXCEPTION: main java.lang.NoClassDefFoundError: (...)

в почти каждой библиотеке нашего проекта.

К счастью, у Google есть другое решение для нас. Мы можем разделить наш проект более чем на один .dex файл и загружать в runtime. Но это процесс не слишком понятный (пару лет назад Google опубликовал статью об этом).

android.support.multidex

Начиная с Android 5.0 Lollipop, появилась новая версия support library в Android SDK. Она вклчает два класса:MultiDex и MultiDexApplication и упрощает процесс загрузки multidex.

Конфигурация проекта

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

В app/build.gradle мы должны добавить:

afterEvaluate {     
tasks.matching
{
it.name.startsWith('dex')
}.each {dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
dx.additionalParameters += "--main-dex-list=$projectDir/<filename>".toString()
}
}

У нас есть два парметра:

  • --multi-dex включает механизм разделения в процесс билда
  • --main-dex-list (не обязательно) - файл со списком классов, который должен прикреплен к dex файлу.

Сейчас закомментируем второй параметр. Мы вернемся к нему позже.

После билда приложения, мы получим несколько dex файлов.

Теперь мы должны смерджить их (загрузить дополнительные dex файлы) внутри нашего приложения. Для начала мы должны приаттачить android-support-multidex.jar библиотеку внтури нашего проекта. Вы можете найти её в: .../Android SDK directory/extras/android/support/multidex/library. После этого у нас есть три способа загрузить .dex files файлы нашего приложения:

  • Задекларировать MultiDexApplication класс в AndroidManifest.xml :

<application android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" android:name="android.support.multidex.MultiDexApplication"> ... </application>

  • Расширить MultiDexApplication для нашего Application класса (Я выбрал этот способ):

public class HelloMultiDexApplication extends MultiDexApplication { @Override public void onCreate() { super.onCreate(); } }

<application android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" android:name=".HelloMultiDexApplication"> ... </application>

  • Если же вы не хотите расширять MultiDexApplication — мы можем установить multiple dex файлы вручную через переопределение attachBaseContext(Context base) метода в нашем Application классе:

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

В целом этой конфигурации должно быть достаточно. Но после билда — мы получим такую ошибку:

UNEXPECTED TOP-LEVEL EXCEPTION: com.android.dex.DexException: Library dex files are not supported in multi-dex mode at com.android.dx.command.dexer.Main.runMultiDex(Main.java:337) at com.android.dx.command.dexer.Main.run(Main.java:243) at com.android.dx.command.dexer.Main.main(Main.java:214) at com.android.dx.command.Main.main(Main.java:106)

Если у вас есть дополнительные библиотеки (в нашем примере у нас есть Facebook SDK) мы должны убедиться что мы отключили pre-dexing для них. К сожалению--multi-dex опция не совместима с pre-dexed библиотеками.

Вы можете сделать это - добавив:

android { // ... dexOptions { preDexLibraries = false } }

внутри вашегоapp/build.gradle файла.

Теперь после билда и запуска все рабоатет как и должно было быть.

Здесь вы найдете commit с полными изменениями необходимыми для включения поддержки MultiDex .

Возможные проблемы

Будьте внимательны у ниже перечисленным пунктам:

  • Дополнительные .dex файлы подгружаются в Application.attachBaseContext(Context) методе (через вызов MultiDex.install(Context) ). Это означает , что перед этим вызовом, мы не можем использовать подгружаемые классы. Те мы не можем задекларировать статические типы, которые располагаются за пределами главного .dex файла. Сделав это мы получим ошибку java.lang.NoClassDefFoundError.
  • Аналогично с методами нашего класса. Мы должны быть уверены , что мы не имеет доступ к классам из вторичных .dex файлов. Это легко обойти , переместив все вызовы во внутренние анонимные классы. Пример, как это можно сделать:

public class HelloMultiDexApplication extends MultiDexApplication { @Override public void onCreate() { super.onCreate(); new Runnable() { @Override public void run() { //Your code } }.run(); } }

Почему мы должны делать именно так? Если коротко, то ClassLoader ищет зависимости во время инициализации класса. Когда мы ищем классы и методы внутри Runnable , вторичные .dex уже загружены.

Другой способ решить проблему — это использование параметры --main-dex-list . Этот параметр позволяет указать классы, окторые нужно поместить в главный .dex файл.

Исходные коды

Полные исходные коды описанного примера, доступны в Github репозитории.

Автор: Miroslaw Stanek

Если вам понравился пост, поделитесь им с вашими подписчиками или подписывайтесь на меня в Twitter!

Ноябрь 2, 2014

--

--