Jak zmniejszyć kod - limit metod 65k w dex


91

Mam dość dużą aplikację na Androida, która opiera się na wielu projektach bibliotecznych. Kompilator Androida ma ograniczenie 65536 metod na plik .dex i przekraczam tę liczbę.

Istnieją zasadniczo dwie ścieżki, które możesz wybrać (przynajmniej o których wiem), gdy osiągniesz limit metody.

1) Zmniejsz swój kod

2) Zbuduj wiele plików dex ( zobacz ten wpis na blogu )

Przyjrzałem się obu i próbowałem dowiedzieć się, co powoduje, że liczba moich metod rośnie tak wysoko. Interfejs API Dysku Google zajmuje największą część z zależnością od guawy na poziomie ponad 12 000. Łączna liczba bibliotek dla interfejsu Drive API v2 przekracza 23 000!

Wydaje mi się, że moje pytanie brzmi, jak myślisz, co powinienem zrobić? Czy powinienem usunąć integrację Dysku Google jako funkcję mojej aplikacji? Czy jest sposób na zmniejszenie API (tak, używam proguard)? Czy powinienem wybrać trasę wielokrotnego dex (co wygląda raczej bolesne, szczególnie w przypadku interfejsów API innych firm)?


2
Tak się składa, że ​​podoba mi się Twoja aplikacja. Czy myślałeś o wykonaniu wymaganego pobrania wszystkich dodatkowych bibliotek w pseudo- apkformie? Osobiście chciałbym zobaczyć integrację z Dyskiem
JBirdVegas

8
Facebook niedawno udokumentował ich obejście tego, co wydaje się prawie identycznym problemem w ich aplikacji na Androida. Może się przydać: facebook.com/notes/facebook-engineering/…
Reuben Scratton

4
Zaczynam podążać trasą wielu deksów. Udało mi się utworzyć dodatkowy plik dex do pracy z Dyskiem Google. Żal mi każdego, kto potrzebuje guawy jako uzależnienia. : P Jest to dla mnie nadal dość duży problem
Jared Rummler

4
jak liczysz metody?
Bri6ko

1
Dodatkowe uwagi tutaj: stackoverflow.com/questions/21490382 (w tym łącze do narzędzia, które wyświetli listę odwołań do metod w pliku APK). Zauważ, że limit 64K nie ma związku z problemem Facebooka, powiązanym z kilkoma komentarzami w górę.
fadden

Odpowiedzi:


69

Wygląda na to, że Google w końcu wdrożył obejście / poprawkę do przekroczenia limitu metody 65K dla plików dex.

O limicie odniesienia 65 tys

Pliki aplikacji na Androida (APK) zawierają wykonywalne pliki kodu bajtowego w postaci plików wykonywalnych Dalvik (DEX), które zawierają skompilowany kod używany do uruchamiania aplikacji. Specyfikacja Dalvik Executable ogranicza całkowitą liczbę metod, do których można odwoływać się w pojedynczym pliku DEX do 65 536, w tym metody platformy Android, metody biblioteczne i metody we własnym kodzie. Przekroczenie tego limitu wymaga skonfigurowania procesu kompilacji aplikacji w celu wygenerowania więcej niż jednego pliku DEX, znanego jako konfiguracja wieluidex.

Obsługa Multidex przed Androidem 5.0

Wersje platformy wcześniejsze niż Android 5.0 używają środowiska wykonawczego Dalvik do wykonywania kodu aplikacji. Domyślnie Dalvik ogranicza aplikacje do jednego pliku z kodem bajtowym classes.dex na plik APK. Aby obejść to ograniczenie, możesz użyć biblioteki obsługującej multidex , która staje się częścią podstawowego pliku DEX aplikacji, a następnie zarządza dostępem do dodatkowych plików DEX i zawartego w nich kodu.

Obsługa Multidex dla Androida 5.0 i nowszych

Android 5.0 lub nowszy używa środowiska wykonawczego o nazwie ART, które natywnie obsługuje ładowanie wielu plików dex z plików APK aplikacji. ART wykonuje prekompilację podczas instalacji aplikacji, która skanuje w poszukiwaniu plików klas (.. N) .dex i kompiluje je w jednym pliku .oat do wykonania na urządzeniu z systemem Android. Aby uzyskać więcej informacji na temat środowiska uruchomieniowego Androida 5.0, zobacz Wprowadzenie do ART .

Zobacz: Tworzenie aplikacji przy użyciu ponad 65 tys. Metod


Biblioteka obsługi Multidex

Ta biblioteka zapewnia obsługę tworzenia aplikacji z wieloma plikami wykonywalnymi Dalvik (DEX). Aplikacje, które odwołują się do więcej niż 65536 metod, są wymagane do korzystania z konfiguracji multidex. Aby uzyskać więcej informacji na temat korzystania z multidex, zobacz Tworzenie aplikacji z ponad 65 tys . Metod .

Ta biblioteka znajduje się w katalogu / extras / android / support / multidex / po pobraniu bibliotek obsługi Androida. Biblioteka nie zawiera zasobów interfejsu użytkownika. Aby uwzględnić go w projekcie aplikacji, postępuj zgodnie z instrukcjami dodawania bibliotek bez zasobów.

Identyfikator zależności skryptu kompilacji Gradle dla tej biblioteki jest następujący:

com.android.support:multidex:1.0.+ Ta notacja zależności określa wydanie w wersji 1.0.0 lub nowszej.


Nadal powinieneś unikać przekroczenia limitu metod 65K poprzez aktywne używanie proguard i przeglądanie swoich zależności.


6
+1, dlaczego ludzie nie głosują za poprawnymi odpowiedziami, gdy odpowiada na nie ta sama osoba?
Pacerier,

minimalny poziom api wynosi 14!
Vihaan Verma

5
Napisaliśmy małą wtyczkę Gradle, aby zapewnić aktualną liczbę metod w każdej kompilacji. Było pomocne dla nas do zarządzania bibliotekami - github.com/KeepSafe/dexcount-gradle-plugin
Philipp

53

możesz użyć do tego biblioteki obsługi multidex, Aby włączyć multidex

1) uwzględnij go w zależnościach:

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

2) Włącz to w swojej aplikacji:

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

3) jeśli masz klasę aplikacji dla swojej aplikacji, Zastąp metodę attachBaseContext w następujący sposób:

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

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

4) Jeśli nie masz klasy aplikacji dla swojej aplikacji, zarejestruj android.support.multidex.MultiDexApplication jako aplikację w pliku manifestu. lubię to:

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

i powinno działać dobrze!


32

Play ServicesPomoc w wersji 6.5+: http://android-developers.blogspot.com/2014/12/google-play-services-and-dex-method.html

„Począwszy od wersji 6.5 Usług Google Play, będzie można wybierać spośród wielu indywidualnych interfejsów API, które można zobaczyć”

...

„będzie to przejściowo obejmować biblioteki„ podstawowe ”, które są używane we wszystkich interfejsach API”.

To dobra wiadomość, na przykład w przypadku prostej gry prawdopodobnie potrzebujesz tylko base, gamesa może drive.

„Pełna lista nazw interfejsów API znajduje się poniżej. Więcej informacji można znaleźć w witrynie programistów Androida:

  • com.google.android.gms: play-services-base: 6.5.87
  • com.google.android.gms: reklamy-usług-odtwarzania: 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: lokalizacja-usług-odtwarzania: 6.5.87
  • com.google.android.gms: usługi-zabaw-fitness: 6.5.87
  • com.google.android.gms: play-services-panorama: 6.5.87
  • com.google.android.gms: dysk usług odtwarzania: 6.5.87
  • com.google.android.gms: gry-usługi-gry: 6.5.87
  • com.google.android.gms: Play-services-wallet: 6.5.87
  • com.google.android.gms: tożsamość-usług-odtwarzania: 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

Jakieś informacje, jak to zrobić w ramach projektu Eclipse?
Brian White

Nie mam jeszcze możliwości uaktualnienia do tej wersji. Ale jeśli twój projekt jest oparty na Maven, to miejmy nadzieję, że musisz to rozwiązać w swoim Maven pom.
Csaba Toth

@ webo80 Cóż, to pomaga tylko w przypadku wersji 6.5.87. Zastanawiam się nad odpowiedzią Peteya, że ​​proguard usuwa nieużywane funkcje. Zastanawiam się, czy dotyczy to również bibliotek drugiej strony, czy tylko twoich własnych rzeczy. Powinienem poczytać więcej o proguard.
Csaba Toth

@BrianWhite Na razie jedynym rozwiązaniem wydaje się usunięcie pliku .jar za pomocą jakiegoś zewnętrznego narzędzia ..
milosmns

1
@CsabaToth, uratowałeś mnie! Dodałem tylko kilka z powyższej listy zamiast pełnego „com.google.android.gms: play-services” i to zrobiło różnicę!
EZDsIt

9

W wersjach usług Google Play starszych niż 6.5 trzeba było skompilować cały pakiet interfejsów API w swojej aplikacji. W niektórych przypadkach utrudniło to utrzymanie liczby metod w aplikacji (w tym interfejsów API platformy, metod bibliotek i własnego kodu) poniżej 65 536 limitu.

Od wersji 6.5 możesz zamiast tego selektywnie kompilować interfejsy API usług Google Play do swojej aplikacji. Na przykład, aby uwzględnić tylko interfejsy API Google Fit i Android Wear, zamień następujący wiersz w pliku build.gradle:

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

z tymi wierszami:

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

Aby uzyskać więcej informacji, kliknij tutaj


Jak to zrobić w zaćmieniu?
Hardik9850

7

Użyj proguard, aby rozjaśnić swój apk, ponieważ nieużywane metody nie będą dostępne w ostatecznej wersji. Dokładnie sprawdź, czy w swoim pliku konfiguracyjnym proguard masz następujące dane, aby używać proguard z guawą (przepraszam, jeśli już to masz, nie było to znane w momencie pisania):

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

Ponadto, jeśli używasz ActionbarSherlock, przełączenie się na bibliotekę obsługującą zgodność aplikacji z wersją 7 również znacznie zmniejszy liczbę metod (w oparciu o osobiste doświadczenia). Instrukcje znajdują się:


wygląda to obiecująco, ale dostałem Warning: butterknife.internal.ButterKnifeProcessor: can't find superclass or interface javax.annotation.processing.AbstractProcessorpodczas biegu./gradlew :myapp:proguardDevDebug
ericn

1
Jednak podczas programowania proguard nie jest generalnie uruchamiany (w końcu nie z Eclipse), więc nie możesz skorzystać z kurczenia się, dopóki nie zrobisz kompilacji wydania.
Brian White

7

Możesz użyć Jar Jar Links, aby zmniejszyć ogromne biblioteki zewnętrzne, takie jak usługi Google Play (metody 16K!)

W twoim przypadku po prostu zgrywasz wszystko z słoika usług Google Play z wyjątkiem common internali drivepod-pakietów.


4

Dla użytkowników Eclipse, którzy nie używają Gradle, istnieją narzędzia, które rozbiją słoik usług Google Play i odbudują go tylko z potrzebnych części.

Używam strip_play_services.sh od dextorer .

Dokładne określenie, które usługi należy uwzględnić, może być trudne, ponieważ istnieją pewne wewnętrzne zależności, ale możesz zacząć od małych elementów i dodać je do konfiguracji, jeśli okaże się, że brakuje potrzebnych elementów.


3

Myślę, że na dłuższą metę najlepszym sposobem byłoby rozbicie aplikacji na wiele sposobów.


2
Szukam odpowiedniego sposobu na zrobienie tego z Gradle: - / Jakaś wskazówka?
Ivan Morgillo


2

Jeśli nie, użyj multidex, co sprawia, że ​​proces budowania jest bardzo powolny. Możesz wykonać następujące czynności. Jak wspomniała Yahska, użyj określonej biblioteki usług Google Play. W większości przypadków tylko to jest potrzebne.

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

Oto wszystkie dostępne pakiety Selektywne kompilowanie interfejsów API do pliku wykonywalnego

Jeśli to nie wystarczy, możesz użyć skryptu gradle. Umieść ten kod w pliku „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
}

}

Następnie zastosuj ten skrypt w pliku build.gradle, w ten sposób

apply plugin: 'com.android.application'
apply from: 'strip_play_services.gradle'

1

Jeśli korzystasz z usług Google Play, możesz wiedzieć, że dodaje 20k + metod. Jak już wspomniano, Android Studio ma opcję modułowego włączania określonych usług, ale użytkownicy, którzy utknęli w Eclipse, muszą wziąć modularyzację w swoje ręce :(

Na szczęście istnieje skrypt powłoki, który ułatwia zadanie. Po prostu wypakuj do katalogu jar usług Google Play, edytuj dostarczony plik .conf zgodnie z potrzebami i wykonaj skrypt powłoki.

Przykład jego użycia znajduje się tutaj .


1

Jeśli korzystasz z usług Google Play, możesz wiedzieć, że dodaje 20k + metod. Jak już wspomniano, Android Studio ma opcję modułowego włączania określonych usług, ale użytkownicy, którzy utknęli w Eclipse, muszą wziąć modularyzację w swoje ręce :(

Na szczęście istnieje skrypt powłoki, który ułatwia zadanie. Po prostu wypakuj do katalogu jar usług Google Play, edytuj dostarczony plik .conf zgodnie z potrzebami i wykonaj skrypt powłoki.

Przykład jego użycia znajduje się tutaj.

Tak jak powiedział, zastępuję compile 'com.google.android.gms:play-services:9.0.0'tylko biblioteki, których potrzebowałem i zadziałało.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.