So verkleinern Sie Code - 65k Methodenlimit in Dex

91

Ich habe eine ziemlich große Android-App, die auf vielen Bibliotheksprojekten basiert. Der Android-Compiler hat eine Beschränkung von 65536 Methoden pro .dex-Datei und ich übertreffe diese Zahl.

Grundsätzlich gibt es zwei Pfade, die Sie auswählen können (zumindest die mir bekannten), wenn Sie das Methodenlimit erreichen.

1) Verkleinern Sie Ihren Code

2) Erstellen Sie mehrere Dex-Dateien ( siehe diesen Blog-Beitrag )

Ich schaute in beide und versuchte herauszufinden, warum meine Methodenanzahl so hoch war. Die Google Drive-API nimmt mit einer Guava-Abhängigkeit von über 12.000 den größten Anteil ein. Die Gesamtbibliotheken für Drive API v2 erreichen über 23.000!

Meine Frage ist wohl, was soll ich tun? Sollte ich die Google Drive-Integration als Funktion meiner App entfernen? Gibt es eine Möglichkeit, die API zu verkleinern (ja, ich verwende Proguard)? Sollte ich den Multiple-Dex-Weg gehen (was ziemlich schmerzhaft aussieht, insbesondere bei APIs von Drittanbietern)?

Jared Rummler
quelle
2
Ich liebe deine App. Haben Sie darüber nachgedacht, alle zusätzlichen Bibliotheken in einer Pseudo- apkForm herunterzuladen ? Ich persönlich würde gerne die Integration von Laufwerken sehen
JBirdVegas
8
Facebook hat kürzlich seine Problemumgehung für ein scheinbar fast identisches Problem in seiner Android-App dokumentiert.
Kann
4
Beginnen Sie, die Mehrfach-Dex-Route hinunterzugehen. Ich habe erfolgreich eine sekundäre Dex-Datei erstellt, um mit Google Drive zu arbeiten. Ich fühle mich schlecht für jeden, der Guave als Abhängigkeit braucht. : P Es ist immer noch ein ziemlich großes Problem für mich
Jared Rummler
4
Wie zählen Sie die Methoden?
Bri6ko
1
Einige zusätzliche Hinweise hier: stackoverflow.com/questions/21490382 (einschließlich eines Links zu einem Dienstprogramm, das die Methodenreferenzen in einer APK auflistet). Beachten Sie, dass das 64K-Limit nichts mit dem Facebook-Problem zu tun hat, das mit einigen Kommentaren verknüpft ist.
Fadden

Antworten:

69

Es sieht so aus, als hätte Google endlich eine Problemumgehung / Korrektur implementiert, um das 65-KB-Methodenlimit für Dex-Dateien zu überschreiten.

Informationen zum 65K-Referenzlimit

APK-Dateien (Android Application) enthalten ausführbare Bytecode-Dateien in Form von DEX-Dateien (Dalvik Executable), die den kompilierten Code enthalten, der zum Ausführen Ihrer App verwendet wird. Die Dalvik Executable-Spezifikation begrenzt die Gesamtzahl der Methoden, auf die in einer einzelnen DEX-Datei verwiesen werden kann, auf 65.536, einschließlich Android-Framework-Methoden, Bibliotheksmethoden und Methoden in Ihrem eigenen Code. Um diese Grenze zu überschreiten, müssen Sie Ihren App-Erstellungsprozess so konfigurieren, dass mehr als eine DEX-Datei generiert wird, die als Multidex-Konfiguration bezeichnet wird.

Multidex-Unterstützung vor Android 5.0

Versionen der Plattform vor Android 5.0 verwenden die Dalvik-Laufzeit zum Ausführen von App-Code. Standardmäßig beschränkt Dalvik Apps auf eine einzelne Klassen.dex-Bytecode-Datei pro APK. Um diese Einschränkung zu umgehen , können Sie die Multidex-Unterstützungsbibliothek verwenden , die Teil der primären DEX-Datei Ihrer App wird und dann den Zugriff auf die zusätzlichen DEX-Dateien und den darin enthaltenen Code verwaltet.

Multidex-Unterstützung für Android 5.0 und höher

Android 5.0 und höher verwendet eine Laufzeit namens ART, die das Laden mehrerer Dex-Dateien aus APK-Anwendungsdateien nativ unterstützt. ART führt zur Anwendungsinstallationszeit eine Vorkompilierung durch, bei der nach .dex-Dateien der Klassen (.. N) gesucht und diese zur Ausführung durch das Android-Gerät in eine einzige .oat-Datei kompiliert werden. Weitere Informationen zur Laufzeit von Android 5.0 finden Sie unter Einführung in ART .

Siehe: Erstellen von Apps mit über 65.000 Methoden


Multidex-Unterstützungsbibliothek

Diese Bibliothek bietet Unterstützung für das Erstellen von Apps mit mehreren DEX-Dateien (Dalvik Executable). Für die Verwendung von Multidex-Konfigurationen sind Apps erforderlich, die auf mehr als 65536 Methoden verweisen. Weitere Informationen zur Verwendung von Multidex finden Sie unter Erstellen von Apps mit über 65.000 Methoden .

Diese Bibliothek befindet sich im Verzeichnis / extras / android / support / multidex /, nachdem Sie die Android-Unterstützungsbibliotheken heruntergeladen haben. Die Bibliothek enthält keine Benutzeroberflächenressourcen. Befolgen Sie die Anweisungen zum Hinzufügen von Bibliotheken ohne Ressourcen, um es in Ihr Anwendungsprojekt aufzunehmen .

Die Abhängigkeits-ID für das Gradle-Build-Skript für diese Bibliothek lautet wie folgt:

com.android.support:multidex:1.0.+ Diese Abhängigkeitsnotation gibt die Release-Version 1.0.0 oder höher an.


Sie sollten es dennoch vermeiden, das 65K-Methodenlimit zu erreichen, indem Sie proguard aktiv verwenden und Ihre Abhängigkeiten überprüfen.

Jared Rummler
quelle
6
+1, Warum stimmen die Leute nicht mit den richtigen Antworten überein, wenn sie von derselben Person beantwortet werden?
Pacerier
min api level wird 14!
Vihaan Verma
5
Wir haben ein kleines Gradle-Plugin geschrieben, um Ihnen die aktuelle Anzahl der Methoden für jeden Build anzuzeigen. War hilfreich für uns, um Bibliotheken zu verwalten - github.com/KeepSafe/dexcount-gradle-plugin
philipp
53

Sie können dazu die Multidex-Unterstützungsbibliothek verwenden, um Multidex zu aktivieren

1) in Abhängigkeiten aufnehmen:

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

2) Aktivieren Sie es in Ihrer App:

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

3) Wenn Sie eine Anwendungsklasse für Ihre App haben, überschreiben Sie die Methode attachBaseContext wie folgt :

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

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

4) , wenn Sie nicht haben eine Anwendung Klasse für Ihre Anwendung dann registrieren android.support.multidex.MultiDexApplication wie Ihre Anwendung in Ihrer Manifest - Datei. so was:

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

und es sollte gut funktionieren!

Prakhar
quelle
31

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

"Ab Version 6.5 der Google Play-Dienste können Sie aus einer Reihe einzelner APIs auswählen und sehen."

...

"Dies schließt transitiv die 'Basis'-Bibliotheken ein, die für alle APIs verwendet werden."

Das sind gute Nachrichten, für ein einfaches Spiel zum Beispiel brauchen Sie wahrscheinlich nur das base, gamesund vielleicht drive.

"Die vollständige Liste der API-Namen finden Sie unten. Weitere Informationen finden Sie auf der Android Developer-Website:

  • com.google.android.gms: Play-Services-Basis: 6.5.87
  • com.google.android.gms: Play-Services-Anzeigen: 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-Standort: 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-Laufwerk: 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-Identität: 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
quelle
Gibt es Informationen dazu in einem Eclipse-Projekt?
Brian White
Ich bin noch nicht in der Lage, auf diese Version zu aktualisieren. Aber wenn Ihr Projekt auf Maven basiert, müssen Sie das hoffentlich nur in Ihrem Maven Pom lösen.
Csaba Toth
@ webo80 Nun, das hilft nur, wenn Sie bis zur Version 6.5.87 sind. Ich wundere mich über Peteys Antwort, dass Proguard unbenutzte Funktionen reduziert. Ich frage mich, ob es sich dabei auch um Bibliotheken der 2. Partei handelt oder nur um Ihre eigenen Sachen. Ich sollte mehr über Proguard lesen.
Csaba Toth
@BrianWhite Die einzige Lösung für den Moment scheint die .jar-Datei mit einem externen Tool zu
entfernen
Am
Brian White
9

In Versionen von Google Play-Diensten vor Version 6.5 mussten Sie das gesamte API-Paket in Ihre App kompilieren. In einigen Fällen war es dadurch schwieriger, die Anzahl der Methoden in Ihrer App (einschließlich Framework-APIs, Bibliotheksmethoden und Ihres eigenen Codes) unter dem Grenzwert von 65.536 zu halten.

Ab Version 6.5 können Sie stattdessen selektiv Google Play-Dienst-APIs in Ihre App kompilieren. Ersetzen Sie beispielsweise die folgende Zeile in Ihrer build.gradle-Datei, um nur die APIs Google Fit und Android Wear einzuschließen:

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

mit diesen Zeilen:

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

Für weitere Informationen können Sie hier klicken

Akshay
quelle
Wie mache ich das in Eclipse?
Hardik9850
7

Verwenden Sie Proguard, um Ihre Apk zu erleichtern, da Methoden, die nicht verwendet werden, nicht in Ihrem endgültigen Build enthalten sind. Überprüfen Sie in Ihrer Proguard-Konfigurationsdatei, ob Sie Proguard mit Guave verwenden können (ich entschuldige mich, wenn Sie dies bereits haben, war es zum Zeitpunkt des Schreibens noch nicht bekannt):

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

Wenn Sie ActionbarSherlock verwenden, wird durch den Wechsel zur v7-Appcompat-Supportbibliothek außerdem die Anzahl Ihrer Methoden erheblich reduziert (basierend auf persönlichen Erfahrungen). Anweisungen befinden sich:

Petey
quelle
Das sieht vielversprechend aus, aber ich habe es Warning: butterknife.internal.ButterKnifeProcessor: can't find superclass or interface javax.annotation.processing.AbstractProcessorbeim Laufen bekommen./gradlew :myapp:proguardDevDebug
Ericn
1
Während der Entwicklung wird Proguard jedoch im Allgemeinen nicht ausgeführt (zuletzt nicht mit Eclipse), sodass Sie erst nach einem Release-Build von der Schrumpfung profitieren können.
Brian White
7

Sie können Jar Jar Links verwenden , um große externe Bibliotheken wie Google Play Services (16K-Methoden!) Zu verkleinern.

In Ihrem Fall werden Sie nur rip alles von Google Play - Dienste jar Ausnahme common internalund driveUnterpakete.

Pixel
quelle
4

Für Eclipse-Benutzer, die Gradle nicht verwenden, gibt es Tools, mit denen das Google Play Services-JAR aufgeschlüsselt und nur mit den gewünschten Teilen neu erstellt werden kann.

Ich benutze strip_play_services.sh von dextorer .

Es kann schwierig sein, genau zu wissen, welche Services eingeschlossen werden sollen, da einige interne Abhängigkeiten bestehen. Sie können jedoch klein anfangen und die Konfiguration erweitern, wenn sich herausstellt, dass die erforderlichen Elemente fehlen.

Brian White
quelle
3

Ich denke, dass es auf lange Sicht der beste Weg wäre, Ihre App in mehreren Dex zu brechen.

prmottajr
quelle
2
Ich suche nach einem geeigneten Weg, dies mit Gradle zu tun: - / Irgendein Hinweis?
Ivan Morgillo
2

Multi-dex Unterstützung sein wird die offizielle Lösung für dieses Problem. Siehe meine Antwort hier für die Details.

Alex Lipov
quelle
2

Wenn Sie nicht Multidex verwenden, wird der Build-Prozess sehr langsam. Sie können Folgendes tun. Wie bereits erwähnt, verwenden Sie eine bestimmte Google Play Service-Bibliothek. In den meisten Fällen wird nur dies benötigt.

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

Hier finden Sie alle verfügbaren Pakete , die APIs selektiv in Ihre ausführbare Datei kompilieren

Wenn dies nicht ausreicht, können Sie das Gradle-Skript verwenden. Fügen Sie diesen Code in die Datei 'strip_play_services.gradle' ein.

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
}

}}

Wenden Sie dann dieses Skript wie folgt in Ihrem build.gradle an

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

Wenn Sie Google Play Services verwenden, wissen Sie möglicherweise, dass mehr als 20.000 Methoden hinzugefügt werden. Wie bereits erwähnt, bietet Android Studio die Möglichkeit, bestimmte Dienste modular einzubeziehen. Benutzer, die an Eclipse festhalten, müssen die Modularisierung jedoch selbst in die Hand nehmen :(

Glücklicherweise gibt es ein Shell-Skript , das die Arbeit ziemlich einfach macht. Extrahieren Sie einfach in das JAR-Verzeichnis von Google Play Services, bearbeiten Sie die mitgelieferte .conf-Datei nach Bedarf und führen Sie das Shell-Skript aus.

Ein Beispiel für seine Verwendung finden Sie hier .

Tom
quelle
1

Wenn Sie Google Play Services verwenden, wissen Sie möglicherweise, dass mehr als 20.000 Methoden hinzugefügt werden. Wie bereits erwähnt, bietet Android Studio die Möglichkeit, bestimmte Dienste modular einzubeziehen. Benutzer, die an Eclipse festhalten, müssen die Modularisierung jedoch selbst in die Hand nehmen :(

Glücklicherweise gibt es ein Shell-Skript, das die Arbeit ziemlich einfach macht. Extrahieren Sie einfach in das JAR-Verzeichnis von Google Play Services, bearbeiten Sie die mitgelieferte .conf-Datei nach Bedarf und führen Sie das Shell-Skript aus.

Ein Beispiel für seine Verwendung finden Sie hier.

Wie er sagte, ersetze ich compile 'com.google.android.gms:play-services:9.0.0'nur durch die Bibliotheken, die ich brauchte und es hat funktioniert.

Tamir Gilany
quelle