Как уменьшить код - ограничение метода 65k в dex

91

У меня довольно большое приложение для Android, которое опирается на многие библиотечные проекты. Компилятор Android имеет ограничение в 65536 методов на файл .dex, и я превышаю это число.

По сути, есть два пути, которые вы можете выбрать (по крайней мере, о которых я знаю), когда вы достигнете предела метода.

1) Уменьшите код

2) Создайте несколько файлов dex ( см. Это сообщение в блоге )

Я изучил оба и попытался выяснить, из-за чего мой счет методов стал таким большим. Google Drive API занимает самый большой кусок с зависимостью Guava - более 12000. Общее количество библиотек для Drive API v2 превышает 23 000!

У меня вопрос, как вы думаете, что мне делать? Следует ли мне удалить интеграцию с Google Диском как функцию моего приложения? Есть ли способ уменьшить API (да, я использую proguard)? Должен ли я пойти по маршруту с несколькими dex (что выглядит довольно болезненно, особенно со сторонними API)?

Джаред Раммлер
источник
2
Мне нравится твое приложение. Вы не задумывались о том, чтобы сделать обязательную загрузку всех дополнительных библиотек в псевдо- apkформе? Я лично хотел бы видеть интеграцию с Drive
JBirdVegas
8
Facebook недавно задокументировал обходной путь для почти идентичной проблемы в своем приложении для Android. Может быть полезно: facebook.com/notes/facebook-engineering/…
Reuben Scratton
4
Начинаем двигаться по маршруту с несколькими локациями. Я успешно создал дополнительный файл dex для работы с Google Диском. Мне жаль всех, кому нужна гуава как зависимость. : P Для меня это все еще довольно большая проблема
Джаред Раммлер
4
как считать методы?
Bri6ko 02
1
Некоторые дополнительные примечания здесь: stackoverflow.com/questions/21490382 (включая ссылку на утилиту, которая будет перечислять ссылки на методы в APK). Обратите внимание, что ограничение в 64 КБ не связано с проблемой Facebook, связанной с несколькими комментариями.
fadden 09

Ответы:

69

Похоже, что Google наконец-то реализовал обходной путь / исправление для превышения ограничения в 65 КБ для dex-файлов.

О эталонном пределе 65 КБ

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

Поддержка Multidex до Android 5.0

Версии платформы до Android 5.0 используют среду выполнения Dalvik для выполнения кода приложения. По умолчанию Dalvik ограничивает приложения одним файлом байт-кода classes.dex для каждого APK. Чтобы обойти это ограничение, вы можете использовать библиотеку поддержки multidex , которая становится частью основного файла DEX вашего приложения и затем управляет доступом к дополнительным файлам DEX и содержащемуся в них коду.

Поддержка Multidex для Android 5.0 и выше

Android 5.0 и выше использует среду выполнения под названием ART, которая изначально поддерживает загрузку нескольких файлов dex из файлов APK приложения. ART выполняет предварительную компиляцию во время установки приложения, которое сканирует файлы классов (.. N) .dex и компилирует их в один файл .oat для выполнения на устройстве Android. Дополнительные сведения о среде выполнения Android 5.0 см. В разделе Знакомство с ART .

См .: Создание приложений с использованием более 65K методов.


Библиотека поддержки Multidex

Эта библиотека обеспечивает поддержку для создания приложений с несколькими исполняемыми файлами Dalvik (DEX). Для использования конфигураций multidex требуются приложения, которые ссылаются на более чем 65536 методов. Дополнительные сведения об использовании multidex см. В разделе Создание приложений с использованием методов более 65 КБ .

Эта библиотека находится в каталоге / extras / android / support / multidex / после загрузки библиотек поддержки Android. Библиотека не содержит ресурсов пользовательского интерфейса. Чтобы включить его в проект приложения, следуйте инструкциям по добавлению библиотек без ресурсов.

Идентификатор зависимости скрипта сборки Gradle для этой библиотеки выглядит следующим образом:

com.android.support:multidex:1.0.+ Это обозначение зависимостей указывает версию выпуска 1.0.0 или выше.


Вам по-прежнему следует избегать превышения лимита метода в 65 КБ, активно используя proguard и просматривая свои зависимости.

Джаред Раммлер
источник
6
+1, Почему люди не голосуют за правильные ответы, если им отвечает один и тот же человек?
Pacerier
минимальный уровень api становится 14!
Вихан Верма,
5
Мы написали небольшой плагин Gradle, чтобы дать вам текущее количество методов для каждой сборки. Нам помогли управлять библиотеками - github.com/KeepSafe/dexcount-gradle-plugin
Филипп
53

вы можете использовать для этого библиотеку поддержки multidex, чтобы включить multidex

1) включить его в зависимости:

dependencies {
  ...
  compile 'com.android.support:multidex:1.0.0'
}

2) Включите его в своем приложении:

defaultConfig {
    ...
    minSdkVersion 14
    targetSdkVersion 21
    ....
    multiDexEnabled true
}

3) если у вас есть класс приложения для вашего приложения, переопределите метод attachBaseContext следующим образом:

package ....;
...
import android.support.multidex.MultiDex;

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

4) если вы не имеют приложения класса для вашего приложения , то зарегистрировать android.support.multidex.MultiDexApplication в качестве приложения в вашем файле манифеста. как это:

<application
    ...
    android:name="android.support.multidex.MultiDexApplication">
    ...
</application>

и должно работать нормально!

Прахар
источник
32

Play Services6.5+ помогает: http://android-developers.blogspot.com/2014/12/google-play-services-and-dex-method.html

«Начиная с версии 6.5 сервисов Google Play, вы сможете выбирать из ряда отдельных API, и вы можете видеть»

...

«это будет временно включать« базовые »библиотеки, которые используются во всех API».

Это хорошие новости, например, для простой игры вам, вероятно, понадобится только base, gamesи, возможно drive.

Полный список имен API приведен ниже. Более подробную информацию можно найти на сайте разработчиков Android .:

  • com.google.android.gms: play-services-base: 6.5.87
  • com.google.android.gms: play-services-ads: 6.5.87
  • com.google.android.gms: play-services-appindexing: 6.5.87
  • com.google.android.gms: play-services-maps: 6.5.87
  • com.google.android.gms: play-services-location: 6.5.87
  • com.google.android.gms: play-services-fitness: 6.5.87
  • com.google.android.gms: play-services-Panorama: 6.5.87
  • com.google.android.gms: play-services-drive: 6.5.87
  • com.google.android.gms: play-services-games: 6.5.87
  • com.google.android.gms: play-services-wallet: 6.5.87
  • com.google.android.gms: play-services-identity: 6.5.87
  • com.google.android.gms: play-services-cast: 6.5.87
  • com.google.android.gms: play-services-plus: 6.5.87
  • com.google.android.gms: play-services-appstate: 6.5.87
  • com.google.android.gms: play-services-wearable: 6.5.87
  • com.google.android.gms: play-services-all-wear: 6.5.87
Чаба Тот
источник
Любая информация о том, как это сделать в проекте Eclipse?
Брайан Уайт,
Я пока не могу перейти на эту версию. Но если ваш проект основан на Maven, то, надеюсь, вам просто нужно решить это в своем maven pom.
Csaba Toth
@ webo80 Ну, это помогает, только если у вас версия до 6.5.87. Меня интересует ответ Пити, что proguard удаляет неиспользуемые функции. Интересно, касается ли это также сторонних библиотек или просто ваших собственных вещей. Мне нужно узнать больше о Proguard.
Csaba Toth
@BrianWhite На данный момент единственное решение - удалить файл .jar с помощью какого-то внешнего инструмента ..
milosmns
1
@CsabaToth, ты меня спас! Я добавил только несколько из приведенного выше списка вместо полного com.google.android.gms: play-services, и это имело значение!
EZDsIt
9

В версиях сервисов Google Play до 6.5 вам нужно было скомпилировать весь пакет API в свое приложение. В некоторых случаях это затрудняло поддержание количества методов в вашем приложении (включая API фреймворка, библиотечные методы и ваш собственный код) в пределах 65 536.

Начиная с версии 6.5, вместо этого вы можете выборочно компилировать API сервисов Google Play в свое приложение. Например, чтобы включить только API Google Fit и Android Wear, замените следующую строку в файле build.gradle:

compile 'com.google.android.gms:play-services:6.5.87'

с этими строками:

compile 'com.google.android.gms:play-services-fitness:6.5.87'
compile 'com.google.android.gms:play-services-wearable:6.5.87'

для получения дополнительной информации вы можете нажать здесь

Акшай
источник
Как это сделать при затмении?
Hardik9850
7

Используйте proguard, чтобы облегчить ваш apk, поскольку неиспользуемые методы не будут включены в вашу окончательную сборку. Дважды проверьте, что у вас есть следующее в вашем конфигурационном файле proguard, чтобы использовать proguard с guava (приношу свои извинения, если у вас уже есть это, на момент написания этого не было известно):

# Guava exclusions (http://code.google.com/p/guava-libraries/wiki/UsingProGuardWithGuava)
-dontwarn sun.misc.Unsafe
-dontwarn com.google.common.collect.MinMaxPriorityQueue
-keepclasseswithmembers public class * {
    public static void main(java.lang.String[]);
} 

# Guava depends on the annotation and inject packages for its annotations, keep them both
-keep public class javax.annotation.**
-keep public class javax.inject.**

Кроме того, если вы используете ActionbarSherlock, переход на библиотеку поддержки appcompat v7 также значительно сократит количество ваших методов (исходя из личного опыта). Инструкции расположены:

Пити
источник
это выглядит многообещающе, но я понял, Warning: butterknife.internal.ButterKnifeProcessor: can't find superclass or interface javax.annotation.processing.AbstractProcessorкогда бежал./gradlew :myapp:proguardDevDebug
Эрикн
1
Однако во время разработки proguard обычно не запускается (наконец, не с Eclipse), поэтому вы не можете получить выгоду от сжатия, пока не выполните сборку релиза.
Брайан Уайт
7

Вы можете использовать Jar Jar Links для сжатия огромных внешних библиотек, таких как Google Play Services (методы 16K!)

В вашем случае вы просто рипы все от Google Play Services банки , за исключением common internalи driveсуб-пакеты.

пиксель
источник
4

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

Я использую strip_play_services.sh от dextorer .

Может быть трудно точно определить, какие службы включить, потому что есть некоторые внутренние зависимости, но вы можете начать с малого и добавить в конфигурацию, если окажется, что необходимые вещи отсутствуют.

Брайан Уайт
источник
3

Я думаю, что в долгосрочной перспективе лучшим способом будет разбить ваше приложение на несколько dex.

Prmottajr
источник
2
Я ищу правильный способ сделать это с помощью Gradle: - / Есть подсказка?
Иван Моргилло 01
2

Поддержка Multi-dex будет официальным решением этой проблемы. Подробности см. В моем ответе .

Алексей Липов
источник
2

Если не использовать multidex, который сильно замедляет процесс сборки. Вы можете сделать следующее. Как упоминалось yahska, используйте определенную библиотеку службы Google Play. В большинстве случаев требуется только это.

compile 'com.google.android.gms:play-services-base:6.5.+'

Вот все доступные пакеты. Выборочная компиляция API в ваш исполняемый файл.

Если этого будет недостаточно, вы можете использовать скрипт Gradle. Поместите этот код в файл strip_play_services.gradle

def toCamelCase(String string) {
String result = ""
string.findAll("[^\\W]+") { String word ->
    result += word.capitalize()
}
return result
}

afterEvaluate { project ->
Configuration runtimeConfiguration = project.configurations.getByName('compile')
println runtimeConfiguration
ResolutionResult resolution = runtimeConfiguration.incoming.resolutionResult
// Forces resolve of configuration
ModuleVersionIdentifier module = resolution.getAllComponents().find {
    it.moduleVersion.name.equals("play-services")
}.moduleVersion


def playServicesLibName = toCamelCase("${module.group} ${module.name} ${module.version}")
String prepareTaskName = "prepare${playServicesLibName}Library"
File playServiceRootFolder = project.tasks.find { it.name.equals(prepareTaskName) }.explodedDir


def tmpDir = new File(project.buildDir, 'intermediates/tmp')
tmpDir.mkdirs()
def libFile = new File(tmpDir, "${playServicesLibName}.marker")

def strippedClassFileName = "${playServicesLibName}.jar"
def classesStrippedJar = new File(tmpDir, strippedClassFileName)

def packageToExclude = ["com/google/ads/**",
                        "com/google/android/gms/actions/**",
                        "com/google/android/gms/ads/**",
                        // "com/google/android/gms/analytics/**",
                        "com/google/android/gms/appindexing/**",
                        "com/google/android/gms/appstate/**",
                        "com/google/android/gms/auth/**",
                        "com/google/android/gms/cast/**",
                        "com/google/android/gms/drive/**",
                        "com/google/android/gms/fitness/**",
                        "com/google/android/gms/games/**",
                        "com/google/android/gms/gcm/**",
                        "com/google/android/gms/identity/**",
                        "com/google/android/gms/location/**",
                        "com/google/android/gms/maps/**",
                        "com/google/android/gms/panorama/**",
                        "com/google/android/gms/plus/**",
                        "com/google/android/gms/security/**",
                        "com/google/android/gms/tagmanager/**",
                        "com/google/android/gms/wallet/**",
                        "com/google/android/gms/wearable/**"]

Task stripPlayServices = project.tasks.create(name: 'stripPlayServices', group: "Strip") {
    inputs.files new File(playServiceRootFolder, "classes.jar")
    outputs.dir playServiceRootFolder
    description 'Strip useless packages from Google Play Services library to avoid reaching dex limit'

    doLast {
        def packageExcludesAsString = packageToExclude.join(",")
        if (libFile.exists()
                && libFile.text == packageExcludesAsString
                && classesStrippedJar.exists()) {
            println "Play services already stripped"
            copy {
                from(file(classesStrippedJar))
                into(file(playServiceRootFolder))
                rename { fileName ->
                    fileName = "classes.jar"
                }
            }
        } else {
            copy {
                from(file(new File(playServiceRootFolder, "classes.jar")))
                into(file(playServiceRootFolder))
                rename { fileName ->
                    fileName = "classes_orig.jar"
                }
            }
            tasks.create(name: "stripPlayServices" + module.version, type: Jar) {
                destinationDir = playServiceRootFolder
                archiveName = "classes.jar"
                from(zipTree(new File(playServiceRootFolder, "classes_orig.jar"))) {
                    exclude packageToExclude
                }
            }.execute()
            delete file(new File(playServiceRootFolder, "classes_orig.jar"))
            copy {
                from(file(new File(playServiceRootFolder, "classes.jar")))
                into(file(tmpDir))
                rename { fileName ->
                    fileName = strippedClassFileName
                }
            }
            libFile.text = packageExcludesAsString
        }
    }
}

project.tasks.findAll {
    it.name.startsWith('prepare') && it.name.endsWith('Dependencies')
}.each { Task task ->
    task.dependsOn stripPlayServices
}
project.tasks.findAll { it.name.contains(prepareTaskName) }.each { Task task ->
    stripPlayServices.mustRunAfter task
}

}

Затем примените этот скрипт в своем build.gradle, например

apply plugin: 'com.android.application'
apply from: 'strip_play_services.gradle'
Роман Назаревич
источник
1

Если вы используете сервисы Google Play, вы можете знать, что он добавляет более 20 тысяч методов. Как уже упоминалось, Android Studio имеет возможность модульного включения определенных сервисов, но пользователи, застрявшие с Eclipse, должны взять модульность в свои руки :(

К счастью, есть сценарий оболочки, который упрощает эту работу. Просто распакуйте в каталог jar сервисов Google Play, отредактируйте предоставленный файл .conf по мере необходимости и выполните сценарий оболочки.

Пример его использования здесь .

Том
источник
1

Если вы используете сервисы Google Play, вы можете знать, что он добавляет более 20 тысяч методов. Как уже упоминалось, Android Studio имеет возможность модульного включения определенных сервисов, но пользователи, застрявшие с Eclipse, должны взять модульность в свои руки :(

К счастью, есть сценарий оболочки, который упрощает эту работу. Просто распакуйте в каталог jar сервисов Google Play, отредактируйте предоставленный файл .conf по мере необходимости и выполните сценарий оболочки.

Пример его использования здесь.

Как он и сказал, я заменяю compile 'com.google.android.gms:play-services:9.0.0'только те библиотеки, которые мне нужны, и это сработало.

Тамир Гилани
источник