Comment réduire le code - Limite de méthode de 65k dans Dex

91

J'ai une application Android assez volumineuse qui repose sur de nombreux projets de bibliothèques. Le compilateur Android a une limitation de 65536 méthodes par fichier .dex et je dépasse ce nombre.

Il y a essentiellement deux chemins que vous pouvez choisir (du moins que je connaisse) lorsque vous atteignez la limite de méthode.

1) Réduisez votre code

2) Créez plusieurs fichiers dex ( voir cet article de blog )

J'ai regardé dans les deux et essayé de découvrir ce qui faisait monter mon nombre de méthodes si haut. L'API Google Drive prend la plus grande part avec la dépendance Guava à plus de 12000. Le nombre total de bibliothèques pour Drive API v2 atteint plus de 23 000!

Ma question est, je suppose, que pensez-vous que je devrais faire? Dois-je supprimer l'intégration de Google Drive en tant que fonctionnalité de mon application? Existe-t-il un moyen de réduire l'API (oui, j'utilise proguard)? Dois-je emprunter la route dex multiple (qui semble plutôt pénible, en particulier avec des API tierces)?

Jared Rummler
la source
2
J'adore votre application. Avez-vous pensé à faire un téléchargement obligatoire de toutes les bibliothèques supplémentaires sous une apkforme pseudo ?
Personnellement,
8
Facebook a récemment documenté sa solution de contournement pour ce qui semble être un problème presque identique dans son application Android. Peut être utile: facebook.com/notes/facebook-engineering/…
Reuben Scratton
4
Commencer à emprunter la route multiple dex. J'ai créé avec succès un fichier dex secondaire pour fonctionner avec Google Drive. Je me sens mal pour quiconque a besoin de goyave comme dépendance. : P C'est toujours un gros problème pour moi
Jared Rummler
4
comment comptez-vous les méthodes?
Bri6ko
1
Quelques notes supplémentaires ici: stackoverflow.com/questions/21490382 (y compris un lien vers un utilitaire qui listera les références de méthode dans un APK). Notez que la limite de 64K n'est pas liée au problème Facebook lié à quelques commentaires.
fadden

Réponses:

69

Il semble que Google ait enfin mis en œuvre une solution de contournement / correctif pour dépasser la limite de méthode de 65K des fichiers dex.

À propos de la limite de référence 65K

Les fichiers d'application Android (APK) contiennent des fichiers de bytecode exécutables sous la forme de fichiers Dalvik Executable (DEX), qui contiennent le code compilé utilisé pour exécuter votre application. La spécification Dalvik Executable limite le nombre total de méthodes pouvant être référencées dans un seul fichier DEX à 65 536, y compris les méthodes du framework Android, les méthodes de bibliothèque et les méthodes dans votre propre code. Pour dépasser cette limite, vous devez configurer le processus de création de votre application pour générer plusieurs fichiers DEX, appelés configuration multidex.

Prise en charge Multidex avant Android 5.0

Les versions de la plate-forme antérieures à Android 5.0 utilisent le runtime Dalvik pour exécuter le code d'application. Par défaut, Dalvik limite les applications à un seul fichier de bytecode classes.dex par APK. Afin de contourner cette limitation, vous pouvez utiliser la bibliothèque de support multidex , qui devient une partie du fichier DEX principal de votre application, puis gère l'accès aux fichiers DEX supplémentaires et au code qu'ils contiennent.

Prise en charge Multidex pour Android 5.0 et supérieur

Android 5.0 et supérieur utilise un environnement d'exécution appelé ART qui prend en charge nativement le chargement de plusieurs fichiers dex à partir de fichiers APK d'application. ART effectue une pré-compilation au moment de l'installation de l'application qui recherche les fichiers de classes (.. N) .dex et les compile en un seul fichier .oat pour exécution par l'appareil Android. Pour plus d'informations sur l'environnement d'exécution Android 5.0, consultez Présentation de ART .

Voir: Créer des applications avec plus de 65 000 méthodes


Bibliothèque de support Multidex

Cette bibliothèque prend en charge la création d'applications avec plusieurs fichiers Dalvik Executable (DEX). Les applications qui font référence à plus de 65536 méthodes sont nécessaires pour utiliser des configurations multidex. Pour plus d'informations sur l'utilisation de multidex, consultez Création d' applications avec plus de 65 000 méthodes .

Cette bibliothèque se trouve dans le répertoire / extras / android / support / multidex / après avoir téléchargé les bibliothèques de support Android. La bibliothèque ne contient pas de ressources d'interface utilisateur. Pour l'inclure dans votre projet d'application, suivez les instructions pour Ajouter des bibliothèques sans ressources.

L'identificateur de dépendance de script de génération Gradle pour cette bibliothèque est le suivant:

com.android.support:multidex:1.0.+ Cette notation de dépendance spécifie la version 1.0.0 ou supérieure.


Vous devriez toujours éviter d'atteindre la limite de la méthode 65K en utilisant activement proguard et en examinant vos dépendances.

Jared Rummler
la source
6
+1, pourquoi les gens ne votent-ils pas pour les bonnes réponses quand ils sont répondus par la même personne?
Pacerier
le niveau minimum de l'API devient 14!
Vihaan Verma
5
Nous avons écrit un petit plugin Gradle pour vous donner le nombre de méthodes actuelles à chaque build. Cela
philipp
53

vous pouvez utiliser la bibliothèque de prise en charge multidex pour cela, pour activer le multidex

1) l' inclure dans les dépendances:

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

2) Activez-le dans votre application:

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

3) si vous avez une classe d' application pour votre application, remplacez la méthode attachBaseContext comme ceci:

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

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

4) si vous n'avez une demande de classe pour votre application puis enregistrez android.support.multidex.MultiDexApplication que votre application dans votre fichier manifeste. comme ça:

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

et cela devrait bien fonctionner!

Prakhar
la source
31

Play Services6.5+ aide: http://android-developers.blogspot.com/2014/12/google-play-services-and-dex-method.html

"À partir de la version 6.5 des services Google Play, vous pourrez choisir parmi plusieurs API individuelles, et vous pourrez voir"

...

"cela inclura de manière transitoire les bibliothèques 'de base', qui sont utilisées dans toutes les API."

C'est une bonne nouvelle, pour un jeu simple par exemple, vous n'avez probablement besoin que du base, gameset peut-être drive.

"La liste complète des noms d'API se trouve ci-dessous. Vous trouverez plus de détails sur le site des développeurs Android:

  • com.google.android.gms: base de services de jeu: 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: emplacement des services de lecture: 6.5.87
  • com.google.android.gms: jeux-services-fitness: 6.5.87
  • com.google.android.gms: panorama des services de jeu: 6.5.87
  • com.google.android.gms: lecteur de services de lecture: 6.5.87
  • com.google.android.gms: jeux-services-jeux: 6.5.87
  • com.google.android.gms: portefeuille-services-de-jeu: 6.5.87
  • com.google.android.gms: identité-services-de-jeu: 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
Csaba Toth
la source
Des informations sur la façon de faire cela dans un projet Eclipse?
Brian White
Je ne suis pas encore en mesure de passer à cette version. Mais si votre projet est basé sur Maven, alors j'espère que vous n'aurez qu'à résoudre cela en vous maven pom.
Csaba Toth
@ webo80 Eh bien, cela n'aide que si vous êtes à la version 6.5.87. Je m'interroge sur la réponse de Petey, que proguard supprime les fonctions inutilisées. Je me demande si cela concerne aussi les bibliothèques du 2e parti, ou simplement vos propres trucs. Je devrais en savoir plus sur proguard.
Csaba Toth
@BrianWhite La seule solution pour l'instant semble supprimer le fichier .jar avec un outil externe ..
milosmns
J'ai fini par utiliser cet outil: gist.github.com/dextorer/a32cad7819b7f272239b
Brian White
9

Dans les versions des services Google Play antérieures à la version 6.5, vous deviez compiler l'intégralité du package d'API dans votre application. Dans certains cas, cela rendait plus difficile de maintenir le nombre de méthodes dans votre application (y compris les API de framework, les méthodes de bibliothèque et votre propre code) sous la limite de 65 536.

À partir de la version 6.5, vous pouvez à la place compiler de manière sélective les API du service Google Play dans votre application. Par exemple, pour inclure uniquement les API Google Fit et Android Wear, remplacez la ligne suivante dans votre fichier build.gradle:

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

avec ces lignes:

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

pour plus de références, vous pouvez cliquer ici

akshay
la source
Comment le faire en éclipse?
Hardik9850
7

Utilisez proguard pour alléger votre apk car les méthodes inutilisées ne seront pas dans votre version finale. Vérifiez que vous avez ce qui suit dans votre fichier de configuration proguard pour utiliser proguard avec la goyave (mes excuses si vous l'avez déjà, cela n'était pas connu au moment de la rédaction):

# 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.**

En outre, si vous utilisez ActionbarSherlock, le passage à la bibliothèque de support v7 appcompat réduira également considérablement le nombre de vos méthodes (en fonction de votre expérience personnelle). Les instructions se trouvent:

Petey
la source
cela semble prometteur mais je l'ai eu Warning: butterknife.internal.ButterKnifeProcessor: can't find superclass or interface javax.annotation.processing.AbstractProcessoren courant./gradlew :myapp:proguardDevDebug
ericn
1
Pendant le développement, cependant, proguard n'est généralement pas exécuté (enfin pas avec Eclipse), vous ne pouvez donc pas bénéficier de la réduction avant de faire une version de version.
Brian White
7

Vous pouvez utiliser Jar Jar Links pour réduire d'énormes bibliothèques externes comme les services Google Play (méthodes 16K!)

Dans votre cas , vous tout simplement rip Google Play jar les services , sauf common internalet drivesous-packages.

pixel
la source
4

Pour les utilisateurs d'Eclipse n'utilisant pas Gradle, il existe des outils qui décomposent le fichier des services Google Play et le reconstruisent avec uniquement les pièces que vous souhaitez.

J'utilise strip_play_services.sh par dextorer .

Il peut être difficile de savoir exactement quels services inclure car il existe des dépendances internes, mais vous pouvez commencer petit et ajouter à la configuration s'il s'avère que les éléments nécessaires manquent.

Brian White
la source
3

Je pense qu'à long terme, casser votre application en plusieurs dex serait le meilleur moyen.

prmottajr
la source
2
Je cherche une bonne façon de faire cela avec Gradle: - / Un indice?
Ivan Morgillo
2

Le support multi-dex sera la solution officielle à ce problème. Voir ma réponse ici pour les détails.

Alex Lipov
la source
2

Sinon, utiliser multidex, ce qui rend le processus de construction très lent. Vous pouvez faire ce qui suit. Comme yahska l'a mentionné, utilisez une bibliothèque de services google play spécifique. Dans la plupart des cas, seul cela est nécessaire.

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

Voici tous les packages disponibles Compilation sélective des API dans votre exécutable

Si cela ne suffit pas, vous pouvez utiliser le script gradle. Mettez ce code dans le fichier '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
}

}

Ensuite, appliquez ce script dans votre build.gradle, comme ceci

apply plugin: 'com.android.application'
apply from: 'strip_play_services.gradle'
Roman Nazarevych
la source
1

Si vous utilisez les services Google Play, vous savez peut-être qu'il ajoute plus de 20 000 méthodes. Comme déjà mentionné, Android Studio a la possibilité d'inclure modulairement des services spécifiques, mais les utilisateurs bloqués avec Eclipse doivent prendre la modularisation en main :(

Heureusement, il existe un script shell qui rend le travail assez facile. Extrayez simplement le répertoire jar des services google play, modifiez le fichier .conf fourni si nécessaire et exécutez le script shell.

Un exemple de son utilisation est ici .

À M
la source
1

Si vous utilisez les services Google Play, vous savez peut-être qu'il ajoute plus de 20 000 méthodes. Comme déjà mentionné, Android Studio a la possibilité d'inclure modulairement des services spécifiques, mais les utilisateurs bloqués avec Eclipse doivent prendre la modularisation en main :(

Heureusement, il existe un script shell qui rend le travail assez facile. Extrayez simplement le répertoire jar des services google play, modifiez le fichier .conf fourni si nécessaire et exécutez le script shell.

Un exemple de son utilisation est ici.

Comme il l'a dit, je remplace compile 'com.google.android.gms:play-services:9.0.0'juste par les bibliothèques dont j'avais besoin et cela a fonctionné.

Tamir Gilany
la source