Włącz multidex w przypadku aplikacji z ponad 64 tys. metod

Jeśli minSdk Twojej aplikacji to interfejs API 20 lub starszy, a aplikacja i biblioteki, do których się odwołuje, przekraczają 65 536 metod, pojawi się ten błąd kompilacji wskazujący, że aplikacja osiągnęła limit architektury kompilacji Androida:

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

Starsze wersje systemu kompilacji zgłaszają inny błąd, który wskazuje na ten sam problem:

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

W przypadku tych błędów wyświetla się wspólny numer: 65536. Ta liczba reprezentuje łączną liczbę odwołań, które mogą być wywoływane przez kod w jednym pliku kodu bajtowego Dalvik Executable (DEX). Na tej stronie wyjaśniamy, jak ominąć to ograniczenie, włączając konfigurację aplikacji o nazwie multidex, która umożliwia aplikacji tworzenie i odczytywanie wielu plików DEX.

Limit 64 tys. odwołań

Pliki aplikacji na Androida (APK) zawierają pliki wykonywalne kodu bajtowego w formacie Dalvik Executable (DEX), które zawierają skompilowany kod używany do uruchamiania aplikacji. Specyfikacja Dalvik Executable ogranicza łączną liczbę metod,do których można się odwoływać w pojedynczym pliku DEX, do 65 536 – w tym metod platformy Android, metod biblioteki i metod we własnym kodzie.

W kontekście informatyki termin kilo lub K oznacza 1024 (czyli 2^10). Ponieważ 65 536 to 64 × 1024, limit ten jest nazywany _limitem 64 tys. odwołań_.

Obsługa multidex przed Androidem 5.0

Wersje platformy starsze niż Android 5.0 (poziom interfejsu API 21) używają środowiska wykonawczego Dalvik do wykonywania kodu aplikacji. Domyślnie Dalvik ogranicza aplikacje do jednego pliku kodu bajtowego classes.dex na plik APK. Aby obejść to ograniczenie, dodaj bibliotekę multidex do pliku build.gradle lub build.gradle.kts na poziomie modułu:

Dynamiczny

dependencies {
    def multidex_version = "2.0.1"
    implementation "androidx.multidex:multidex:$multidex_version"
}

Kotlin

dependencies {
    val multidex_version = "2.0.1"
    implementation("androidx.multidex:multidex:$multidex_version")
}

Ta biblioteka staje się częścią głównego pliku DEX aplikacji, a następnie zarządza dostępem do dodatkowych plików DEX i zawartego w nich kodu. Aby wyświetlić bieżące wersje tej biblioteki, zobacz wersje multidex.

Więcej informacji znajdziesz w sekcji o konfigurowaniu aplikacji pod kątem multidex.

Obsługa multidex na Androidzie 5.0 i nowszych

Android 5.0 (poziom interfejsu API 21) i nowsze wersje używają środowiska wykonawczego o nazwie ART, które natywnie obsługuje wczytywanie wielu plików DEX z plików APK. ART przeprowadza wstępną kompilację podczas instalacji aplikacji, skanując pliki classesN.dex i kompilując je w jeden plik OAT, który może być wykonywany przez urządzenie z Androidem. Dlatego jeśli minSdkVersion ma wartość 21 lub wyższą, multidex jest domyślnie włączony i nie potrzebujesz biblioteki multidex.

Więcej informacji o środowisku wykonawczym Androida 5.0 znajdziesz w artykule Środowisko wykonawcze Androida (ART) i Dalvik.

Uwaga: gdy uruchamiasz aplikację za pomocą Android Studio, kompilacja jest optymalizowana pod kątem urządzeń docelowych, na których ją wdrażasz. Obejmuje to włączenie multidex, gdy urządzenia docelowe mają Androida 5.0 lub nowszego. Ta optymalizacja jest stosowana tylko podczas wdrażania aplikacji za pomocą Android Studio, więc aby uniknąć limitu 64 K, nadal może być konieczne skonfigurowanie kompilacji do publikacji pod kątem multidex.

Unikanie limitu 64 tys.

Zanim skonfigurujesz aplikację, aby umożliwić korzystanie z co najmniej 64 tys. odwołań do metod, podejmij działania mające na celu zmniejszenie łącznej liczby odwołań wywoływanych przez kod aplikacji, w tym metod zdefiniowanych przez kod aplikacji lub dołączone biblioteki.

Te strategie mogą pomóc Ci uniknąć osiągnięcia limitu odwołań DEX:

Sprawdzanie bezpośrednich i pośrednich zależności aplikacji
Zastanów się, czy wartość dużej zależności biblioteki, którą dołączasz do aplikacji, przewyższa ilość kodu dodawanego do aplikacji. Częstym, ale problematycznym wzorcem jest dołączanie bardzo dużej biblioteki, ponieważ kilka metod użytkowych było przydatnych. Zmniejszenie zależności kodu aplikacji może często pomóc w uniknięciu limitu odwołań DEX.
Usuwanie nieużywanego kodu za pomocą R8
Włącz zmniejszanie kodu, aby uruchamiać R8 w kompilacjach do publikacji. Włącz zmniejszanie, aby mieć pewność, że nie dostarczasz nieużywanego kodu w plikach APK. Jeśli zmniejszanie kodu jest prawidłowo skonfigurowane, może też usuwać nieużywany kod i zasoby z zależności.

Dzięki tym technikom możesz zmniejszyć ogólny rozmiar pliku APK i uniknąć konieczności używania multidex w aplikacji.

Konfigurowanie aplikacji pod kątem multidex

Uwaga: jeśli minSdkVersion jest ustawiony na 21 lub więcej, multidex jest domyślnie włączony i nie potrzebujesz biblioteki multidex.

Jeśli parametr minSdkVersion jest ustawiony na 20 lub mniej, musisz użyć biblioteki multidex i wprowadzić w projekcie aplikacji te zmiany:

  1. Zmodyfikuj plik build.gradle na poziomie modułu, aby włączyć multidex i dodać bibliotekę multidex jako zależność, jak pokazano poniżej:

    Dynamiczny

    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 36
            multiDexEnabled true
        }
        ...
    }
    
    dependencies {
        implementation "androidx.multidex:multidex:2.0.1"
    }

    Kotlin

    android {
        defaultConfig {
            ...
            minSdk = 15 
            targetSdk = 36
            multiDexEnabled = true
        }
        ...
    }
    
    dependencies {
        implementation("androidx.multidex:multidex:2.0.1")
    }
  2. W zależności od tego, czy zastąpisz klasę Application, wykonaj jedną z tych czynności:
    • Jeśli nie zastąpisz klasy Application, zmodyfikuj plik manifestu, aby ustawić android:name w tagu <application> w ten sposób:

      <?xml version="1.0" encoding="utf-8"?>
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.myapp">
          <application
                  android:name="androidx.multidex.MultiDexApplication" >
              ...
          </application>
      </manifest>
    • Jeśli zastąpisz klasę Application, zmień ją tak, aby rozszerzała klasę MultiDexApplication, w ten sposób:

      Kotlin

      class MyApplication : MultiDexApplication() {...}

      Java

      public class MyApplication extends MultiDexApplication { ... }
    • Jeśli zastąpisz klasę Application, ale nie możesz zmienić klasy bazowej, zastąp metodę attachBaseContext() i wywołaj MultiDex.install(this), aby włączyć multidex:

      Kotlin

      class MyApplication : SomeOtherApplication() {
      
          override fun attachBaseContext(base: Context) {
              super.attachBaseContext(base)
              MultiDex.install(this)
          }
      }

      Java

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

      Uwaga: nie wykonuj MultiDex.install() ani żadnego innego kodu za pomocą refleksji ani JNI, dopóki nie zakończy się MultiDex.install(). Śledzenie multidex nie będzie podążać za tymi wywołaniami, co spowoduje błędy ClassNotFoundException lub błędy weryfikacji z powodu nieprawidłowego podziału klas między plikami DEX.

Teraz, gdy tworzysz aplikację, narzędzia do kompilacji Androida tworzą główny plik DEX (classes.dex) i w razie potrzeby pliki DEX obsługujące (classes2.dex, classes3.dex itd.). Następnie system kompilacji pakuje wszystkie pliki DEX do pliku APK.

W czasie działania interfejsy API multidex nie przeszukują tylko głównego pliku classes.dex, ale używają specjalnego programu ładującego klasy, aby wyszukać Twoje metody we wszystkich dostępnych plikach DEX.

Ograniczenia biblioteki multidex

Biblioteka multidex ma pewne znane ograniczenia. Podczas dodawania biblioteki do konfiguracji kompilacji aplikacji weź pod uwagę te kwestie:

  • Instalacja plików DEX podczas uruchamiania na partycji danych urządzenia jest złożona i może powodować błędy „Aplikacja nie odpowiada” (ANR), jeśli dodatkowe pliki DEX są duże. Aby uniknąć tego problemu, włącz zmniejszanie kodu, aby zminimalizować rozmiar plików DEX i usunąć nieużywane fragmenty kodu.
  • W przypadku wersji starszych niż Android 5.0 (poziom interfejsu API 21) użycie multidex nie wystarczy, aby obejść limit linearalloc (problem 37008143). Ten limit został zwiększony w Androidzie 4.0 (poziom interfejsu API 14), ale nie rozwiązało to problemu całkowicie.

    W wersjach starszych niż Android 4.0 możesz osiągnąć limit linearalloc, zanim osiągniesz limit indeksu DEX. Jeśli więc kierujesz aplikację na poziomy interfejsu API niższe niż 14, dokładnie przetestuj ją na tych wersjach platformy, ponieważ mogą wystąpić problemy podczas uruchamiania aplikacji lub wczytywania określonych grup klas.

    Zmniejszanie kodu może ograniczyć lub nawet wyeliminować te problemy.

Deklarowanie klas wymaganych w głównym pliku DEX

Podczas tworzenia każdego pliku DEX dla aplikacji multidex narzędzia do kompilacji podejmują złożone decyzje, aby określić, które klasy są potrzebne w głównym pliku DEX, aby aplikacja mogła się pomyślnie uruchomić. Jeśli w głównym pliku DEX nie ma żadnej klasy wymaganej podczas uruchamiania, aplikacja ulega awarii i wyświetla błąd java.lang.NoClassDefFoundError.

Narzędzia do kompilacji rozpoznają ścieżki kodu, do którego dostęp jest uzyskiwany bezpośrednio z kodu aplikacji. Ten problem może jednak wystąpić, gdy ścieżki kodu są mniej widoczne, np. gdy używana biblioteka ma złożone zależności. Jeśli na przykład kod używa introspekcji lub wywołania metod Javy z kodu natywnego, te klasy mogą nie zostać rozpoznane jako wymagane w głównym pliku DEX.

Jeśli pojawi się błąd java.lang.NoClassDefFoundError, musisz ręcznie określić dodatkowe klasy wymagane w głównym pliku DEX, deklarując je za pomocą właściwości multiDexKeepProguard w typie kompilacji. Jeśli klasa jest dopasowana w pliku multiDexKeepProguard, jest dodawana do głównego pliku DEX.

Właściwość multiDexKeepProguard

Plik multiDexKeepProguard używa tego samego formatu co ProGuard i obsługuje całą gramatykę ProGuard. Więcej informacji o tym, jak dostosować to, co jest przechowywane w aplikacji, znajdziesz w artykule o dostosowywaniu kodu do zachowania.

Plik określony w multiDexKeepProguard powinien zawierać opcje -keep w dowolnej prawidłowej składni ProGuard. Na przykład: -keep com.example.MyClass.class. Możesz utworzyć plik o nazwie multidex-config.pro, który wygląda tak:

-keep class com.example.MyClass
-keep class com.example.MyClassToo

Jeśli chcesz określić wszystkie klasy w pakiecie, plik będzie wyglądał tak:

-keep class com.example.** { *; } // All classes in the com.example package

Następnie możesz zadeklarować ten plik dla typu kompilacji w ten sposób:

Dynamiczny

android {
    buildTypes {
        release {
            multiDexKeepProguard file('multidex-config.pro')
            ...
        }
    }
}

Kotlin

android {
    buildTypes {
        getByName("release") {
            multiDexKeepProguard = file("multidex-config.pro")
            ...
        }
    }
}

Optymalizacja multidex w kompilacjach deweloperskich

Konfiguracja multidex znacznie wydłuża czas przetwarzania kompilacji, ponieważ system kompilacji musi podejmować złożone decyzje dotyczące tego, które klasy muszą być uwzględnione w głównym pliku DEX, a które mogą być uwzględnione w plikach pomocniczych. Oznacza to, że kompilacje przyrostowe z użyciem multidex zwykle trwają dłużej i mogą spowolnić proces programowania.

Aby skrócić czas kompilacji przyrostowej, użyj konwertowania do plików .dex, co pozwoli na ponowne wykorzystanie danych wyjściowych multidex między kompilacjami. Konwertowanie do plików .dex opiera się na formacie ART dostępnym tylko na Androidzie 5.0 (poziom interfejsu API 21) i nowszych wersjach. Jeśli używasz Android Studio, IDE automatycznie stosuje konwertowanie do plików .dex podczas wdrażania aplikacji na urządzeniu z Androidem 5.0 (API na poziomie 21) lub nowszym. Jeśli jednak uruchamiasz kompilacje Gradle z wiersza poleceń, musisz ustawić minSdkVersion na 21 lub wyższą, aby włączyć konwertowanie do plików .dex.

Aby zachować ustawienia kompilacji produkcyjnej, możesz utworzyć 2 wersje aplikacji za pomocą wariantów usługi – jedną z wariantem programistycznym, a drugą z wariantem wersji – z różnymi wartościami minSdkVersion, jak pokazano poniżej:

Dynamiczny

android {
    defaultConfig {
        ...
        multiDexEnabled true
        // The default minimum API level you want to support.
        minSdkVersion 15
    }
    productFlavors {
        // Includes settings you want to keep only while developing your app.
        dev {
            // Enables pre-dexing for command-line builds. When using
            // Android Studio 2.3 or higher, the IDE enables pre-dexing
            // when deploying your app to a device running Android 5.0
            // (API level 21) or higher, regardless of minSdkVersion.
            minSdkVersion 21
        }
        prod {
            // If you've configured the defaultConfig block for the production version of
            // your app, you can leave this block empty and Gradle uses configurations in
            // the defaultConfig block instead. You still need to include this flavor.
            // Otherwise, all variants use the "dev" flavor configurations.
        }
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                                 'proguard-rules.pro'
        }
    }
}
dependencies {
    implementation "androidx.multidex:multidex:2.0.1"
}

Kotlin

android {
    defaultConfig {
        ...
        multiDexEnabled = true
        // The default minimum API level you want to support.
        minSdk = 15
    }
    productFlavors {
        // Includes settings you want to keep only while developing your app.
        create("dev") {
            // Enables pre-dexing for command-line builds. When using
            // Android Studio 2.3 or higher, the IDE enables pre-dexing
            // when deploying your app to a device running Android 5.0
            // (API level 21) or higher, regardless of minSdkVersion.
            minSdk = 21
        }
        create("prod") {
            // If you've configured the defaultConfig block for the production version of
            // your app, you can leave this block empty and Gradle uses configurations in
            // the defaultConfig block instead. You still need to include this flavor.
            // Otherwise, all variants use the "dev" flavor configurations.
        }
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android.txt"),
                                                 "proguard-rules.pro")
        }
    }
}

dependencies {
    implementation("androidx.multidex:multidex:2.0.1")
}

Aby dowiedzieć się więcej o strategiach zwiększania szybkości kompilacji w Android Studio lub wierszu poleceń, przeczytaj artykuł Optymalizowanie szybkości kompilacji. Więcej informacji o używaniu wariantów kompilacji znajdziesz w artykule o konfigurowaniu wariantów kompilacji.

Wskazówka: jeśli masz różne warianty kompilacji dla różnych potrzeb multidex, możesz podać inny plik manifestu dla każdego wariantu, tak aby tylko plik dla interfejsu API na poziomie 20 i niższym zmieniał nazwę tagu <application>. Możesz też utworzyć inną podklasę Application dla każdego wariantu, aby tylko podklasa dla interfejsu API na poziomie 20 i niższym rozszerzała klasę MultiDexApplication lub wywoływała MultiDex.install(this).

Testowanie aplikacji multidex

Jeśli piszesz testy z instrumentacją dla aplikacji multidex, nie musisz wykonywać żadnych dodatkowych czynności konfiguracyjnych, o ile używasz instrumentacji MonitoringInstrumentation lub AndroidJUnitRunner. Jeśli używasz innego elementu Instrumentation, musisz zastąpić jego metodę onCreate() tym kodem:

Kotlin

fun onCreate(arguments: Bundle) {
  MultiDex.install(targetContext)
  super.onCreate(arguments)
  ...
}

Java

public void onCreate(Bundle arguments) {
  MultiDex.install(getTargetContext());
  super.onCreate(arguments);
  ...
}