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

Jeśli Twoja aplikacja ma interfejs minSdk API w wersji 20 lub starszej, a aplikacja i biblioteki, do których się ona odnosi, przekraczają 65 536 metod, wystąpi ten błąd kompilacji. Oznacza on, ż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

Te warunki błędów wyświetlają wspólną liczbę: 65 536. Ta liczba jest równa łącznej liczbie odwołań, które mogą być wywoływane przez kod w pojedynczym pliku bajtowym Dalvik Executable (DEX). Z tej strony dowiesz się, jak obejść to ograniczenie, włączając konfigurację aplikacji zwaną multidex, która umożliwia aplikacji tworzenie i odczytywanie wielu plików DEX.

Limit referencyjny 64 K

Pliki aplikacji na Androida (APK) zawierają pliki bajtowe w postaci plików wykonywalnych Dalvik (DEX), które zawierają skompilowany kod używany do uruchamiania aplikacji. Specyfikacja pliku wykonywalnego Dalvik ogranicza łączną liczbę metod, do których można się odwoływać w pojedynczym pliku DEX, do 65 536, w tym metody frameworka Androida, metody bibliotek i metody w Twoim własnym kodzie.

W kontekście informatyki określenie kilo lub K oznacza 1024 (czyli 2^10). Ponieważ 65 536 = 64 x 1024, ten limit jest nazywany _limitem odniesienia 64 K_.

Obsługa Multidex w wersjach starszych niż Android 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 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:

Groovy

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")
}

Biblioteka ta staje się częścią głównego pliku DEX aplikacji, a potem zarządza dostępem do dodatkowych plików DEX i zawieranych przez nie kodów. Aby wyświetlić bieżące wersje tej biblioteki, zapoznaj się z  wersjami multidex.

Więcej informacji znajdziesz w sekcji Konfigurowanie aplikacji na potrzeby multidexu.

Obsługa Multidex w Androidzie 5.0 i nowszych

Android 5.0 (poziom interfejsu API 21) i nowsze wersje korzystają z platformy wykonawalnej ART, która obsługuje natywną obsługę wczytywania wielu plików DEX z plików APK. ART przeprowadza wstępną kompilację w momencie instalowania aplikacji, skanując pliki classesN.dex i kompilując je w jeden plik OAT, który będzie wykonywany przez urządzenie z Androidem. Dlatego jeśli minSdkVersion wynosi 21 lub więcej, 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: jeśli uruchamiasz aplikację w Android Studio, kompilacja jest optymalizowana pod kątem urządzeń docelowych, na których wdrażasz aplikację. Obejmuje to włączenie multidexu na urządzeniach docelowych z Androidem 5.0 lub nowszym. Ta optymalizacja jest stosowana tylko podczas wdrażania aplikacji za pomocą Android Studio, dlatego nadal może być konieczne skonfigurowanie wersji przeznaczonej do publikacji pod kątem multidexu, aby uniknąć limitu 64 KB.

Unikaj limitu 64 K

Zanim skonfigurujesz aplikację, aby umożliwić korzystanie z 64 tys. lub więcej odwołań do metod, wykonaj czynności mające na celu zmniejszenie łącznej liczby odwołań wywoływanych przez kod aplikacji, w tym metod zdefiniowanych przez kod aplikacji lub załączone biblioteki.

Poniższe strategie pomogą Ci uniknąć osiągnięcia limitu plików referencyjnych DEX:

Sprawdzanie bezpośrednich i pośrednich zależności aplikacji
Zastanów się, czy wartość dużej biblioteki, którą uwzględniasz w aplikacji, przeważa nad ilością kodu dodanego do aplikacji. Częstym, ale problematycznym wdrożeniem jest uwzględnienie bardzo dużej biblioteki, ponieważ kilka metod pomocniczych było przydatnych. Zmniejszenie liczby zależności kodu aplikacji często pomaga uniknąć przekroczenia limitu odwołań DEX.
Usuwanie nieużywanego kodu za pomocą R8
Włącz kompresowanie kodu, aby korzystać z R8 w kompilacji do publikacji. Włącz kompresowanie, aby mieć pewność, że nie wysyłasz z plikami APK nieużywanego kodu. 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 stosowania w aplikacji multideksów.

Konfigurowanie aplikacji pod kątem obsługi środowiska Multidex

Uwaga: jeśli wartość minSdkVersion jest równa 21 lub wyższa, multidex jest domyślnie włączony i nie potrzebujesz biblioteki multidex.

Jeśli wartość minSdkVersion jest równa 20 lub mniejsza, 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 tutaj:

    Groovy

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

    Kotlin

    android {
        defaultConfig {
            ...
            minSdk = 15 
            targetSdk = 33
            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 w tagu <application> ustaw android:name 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ępujesz klasę Application, zmień ją, aby rozszerzyć zakres 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 zamiast tego metodę attachBaseContext() i wywołaj funkcję 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: zanim wykonasz MultiDex.install() lub jakikolwiek inny kod za pomocą mechanizmu odbicia lub JNI, upewnij się, że MultiDex.install() został ukończony. Śledzenie wielodex nie będzie śledzić tych wywołań, co spowoduje ClassNotFoundException lub zweryfikowanie błędów z powodu nieprawidłowej partycji klas między plikami DEX.

Gdy kompilujesz aplikację, narzędzia do kompilacji Androida tworzą podstawowy plik DEX (classes.dex) oraz dodatkowe pliki DEX (classes2.dex, classes3.dex itd.) w razie potrzeby. Następnie system kompilacji umieszcza wszystkie pliki DEX w pliku APK.

Podczas działania zamiast wyszukiwać tylko w pliku głównym classes.dex interfejsy API multidex używają specjalnego ładowarki klas do wyszukiwania metod we wszystkich dostępnych plikach DEX.

Ograniczenia biblioteki multidex

Biblioteka Multidex ma pewne znane ograniczenia. Podczas włączania biblioteki do konfiguracji kompilacji aplikacji należy wziąć pod uwagę te kwestie:

  • Instalowanie plików DEX podczas uruchamiania na partycji danych urządzenia jest skomplikowane 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.
  • Jeśli korzystasz z wersji starszych niż Android 5.0 (poziom interfejsu API 21), korzystanie z multidex nie wystarczy, aby obejść limit przydziału liniowego (problem 37008143). Ten limit został zwiększony w Androidzie 4.0 (poziom interfejsu API 14), ale nie rozwiązało to całkowicie problemu.

    W wersjach starszych niż Android 4.0 możesz osiągnąć limit linearalloc przed osiągnięciem limitu indeksu DEX. Jeśli więc kierujesz aplikację na poziomy API niższe niż 14, przeprowadź dokładne testy na tych wersjach platformy, ponieważ aplikacja może mieć problemy podczas uruchamiania lub gdy są ładowane określone grupy klas.

    Skrócenie kodu może zmniejszyć lub całkowicie wyeliminować te problemy.

Zadeklaruj klasy wymagane w głównym pliku DEX.

Podczas kompilowania każdego pliku DEX w przypadku aplikacji wielodrożnej 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ę uruchomić. Jeśli w głównym pliku DEX nie zostanie podana żadna klasa wymagana podczas uruchamiania, aplikacja ulegnie awarii i wystąpi błąd java.lang.NoClassDefFoundError.

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

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

Właściwość multiDexKeepProguard

Plik multiDexKeepProguard ma ten sam format co ProGuard i obsługuje całą gramatykę ProGuard. Więcej informacji o dostosowywaniu tego, co ma być przechowywane w aplikacji, znajdziesz w artykule Dostosowywanie kodu do przechowywania.

Plik podany w parametry multiDexKeepProguard powinien zawierać opcje -keep w dowolnej poprawnej 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:

Odlotowe

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

Kotlin

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

Optymalizacja multideksów w kompilowanych wersjach na potrzeby testów

Konfiguracja multidex wymaga znacznie dłuższego czasu przetwarzania kompilacji, ponieważ system kompilacji musi podejmować złożone decyzje dotyczące tego, które klasy muszą być uwzględnione w podstawowym pliku DEX i które klasy mogą być uwzględnione w dodatkowych plikach DEX. Oznacza to, że kompilacje przyrostowe korzystające z multidexu zwykle trwają dłużej i mogą spowolnić proces tworzenia.

Aby ograniczyć wydłużenie czasu przyrostowego kompilacji, użyj funkcji predexing w celu ponownego wykorzystywania danych wyjściowych w trybie multidex między kompilacjami. Przedwstępne deksowanie polega na korzystaniu z formatu ART, który jest dostępny tylko na Androidzie 5.0 (poziom interfejsu API 21) i nowszych. Jeśli używasz Android Studio, IDE automatycznie stosuje predeksowanie podczas wdrażania aplikacji na urządzenie z Androidem 5.0 (poziom interfejsu API 21) lub nowszym. Jeśli jednak uruchamiasz kompilacje Gradle z wiersza poleceń, musisz ustawić wartość parametru minSdkVersion na 21 lub wyższą, aby umożliwić predeksowanie.

Aby zachować ustawienia kompilacji produkcyjnej, możesz utworzyć 2 wersje aplikacji z wykorzystaniem rodzajów usług – jedną z wersją deweloperską i drugą z rodzajami wersji – z różnymi wartościami atrybutu minSdkVersion, jak pokazano poniżej:

Groovy

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 w wierszu poleceń, przeczytaj artykuł Optymalizacja szybkości kompilacji. Więcej informacji o używaniu wariantów kompilacji znajdziesz w artykule Konfigurowanie wariantów kompilacji.

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

Testowanie aplikacji multidex

Podczas pisania testów pomiarowych aplikacji wieloplatformowych nie trzeba nic konfigurować, jeśli używasz MonitoringInstrumentation lub AndroidJUnitRunner pomiarów. Jeśli używasz innej biblioteki Instrumentation, musisz zastąpić 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);
  ...
}