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

Jeśli Twoja aplikacja ma minSdk interfejsu API na poziomie 20 lub niższym, a Twoja aplikacja i biblioteki, do których się ona odwołuje, przekraczają limit 65 536 metod, pojawi się ten błąd kompilacji, który wskazuje, ż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łędu mają wspólną liczbę: 65536. Ta liczba reprezentuje łączną liczbę odwołań, które mogą być wywoływane przez kod w pojedynczym pliku z kodem bajtowym Dalvik Executable (DEX). Na tej stronie wyjaśniamy, jak obejść to ograniczenie przez włączenie konfiguracji aplikacji o nazwie multidex, która umożliwia aplikacji tworzenie i odczytywanie wielu plików DEX.

Limit 64 tys. plików referencyjnych

Pliki aplikacji na Androida (APK) zawierają wykonywalne pliki z kodem bajtowym w postaci plików 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 w metodach platformy Androida, metodach biblioteki i metodach biblioteki.

W kontekście informatyki termin kilo, czyli K, oznacza 1024 (czyli 2^10). Ponieważ 65 536 to 64 x 1024, ten limit jest nazywany _64 tys. limitu referencyjnego_.

Obsługa Multidex w wersjach starszych niż Android 5.0

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

Odlotowy

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ą podstawowego pliku DEX aplikacji i zarządza dostępem do dodatkowych plików DEX i ich kodu. Aby wyświetlić bieżące wersje tej biblioteki, przeczytaj artykuł o wersjach multidex.

Więcej informacji znajdziesz w sekcji dotyczącej konfigurowania aplikacji pod kątem multidex.

Obsługa Multidex na Androidzie 5.0 i nowszych

Android 5.0 (poziom interfejsu API 21) i nowsze 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ę w czasie instalowania aplikacji, skanuje pliki classesN.dex i kompilowa je w jeden plik OAT do wykonania przez urządzenie z Androidem. Jeśli więc 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 Android Runtime (ART) i Dalvik.

Uwaga: gdy uruchamiasz aplikację przy użyciu Android Studio, kompilacja jest optymalizowana pod kątem urządzeń docelowych, na których ją wdrażasz. Obejmuje to włączenie multidex, gdy na urządzeniach docelowych jest zainstalowany Android 5.0 lub nowszy. Ta optymalizacja jest stosowana tylko podczas wdrażania aplikacji za pomocą Android Studio, więc i tak może być konieczne skonfigurowanie kompilacji wersji na potrzeby multidex, aby uniknąć limitu rozmiaru 64 tys.

Unikaj limitu 64 tys.

Zanim skonfigurujesz w swojej aplikacji korzystanie z co najmniej 64 tys. odwołań do metod, podejmij kroki, aby zmniejszyć łączną liczbę odwołań wywoływanych przez kod aplikacji, w tym metod zdefiniowanych w kodzie aplikacji lub dołączonych bibliotek.

Te strategie pomogą Ci uniknąć przekroczenia limitu plików referencyjnych DEX:

Przeglądanie zależności bezpośrednich i przechodnich aplikacji
Zastanów się, czy wartość dużej zależności od biblioteki, którą umieszczasz w aplikacji, przewyższa ilość kodu dodawanego do aplikacji. Częstym, lecz problematycznym wzorcem jest uwzględnianie bardzo dużej biblioteki, ponieważ przydało się kilka przydatnych metod. Zmniejszenie zależności kodu aplikacji często pozwala uniknąć limitu odwołań do plików DEX.
Usuń nieużywany kod, używając wersji R8
Włącz zmniejszanie kodu, aby uruchamiać R8 w swoich kompilacjach wersji. Włącz zmniejszanie, by nie przesyłać nieużywanego kodu w plikach APK. Jeśli zmniejszanie kodu jest skonfigurowane prawidłowo, może też usunąć nieużywany kod i zasoby z zależności.

Stosowanie tych metod może Ci pomóc zmniejszyć ogólny rozmiar pliku APK i uniknąć potrzeby stosowania w aplikacji Multidex.

Konfigurowanie aplikacji pod kątem multidex

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

Jeśli minSdkVersion ma wartość 20 lub mniejszą, musisz użyć biblioteki multidex i wprowadzić te zmiany w projekcie aplikacji:

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

    Odlotowy

    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 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ą, aby rozszerzyć MultiDexApplication w ten sposób:

      Kotlin

      class MyApplication : MultiDexApplication() {...}
      

      Java

      public class MyApplication extends MultiDexApplication { ... }
      
    • Jeśli zastąpisz klasę Application, ale nie będzie można zmienić klasy podstawowej, zamiast tego 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 uruchamiaj MultiDex.install() ani żadnego innego kodu przez odbicie ani JNI przed zakończeniem działania MultiDex.install(). Śledzenie multidex nie śledzi tych wywołań, co powoduje ClassNotFoundException lub błędy weryfikacji z powodu nieprawidłowego podziału klas między plikami DEX.

Teraz, gdy tworzysz aplikację, narzędzia Android Build Tools konstruują główny plik DEX (classes.dex) i obsługujące pliki DEX (classes2.dex, classes3.dex itd.) w razie potrzeby. System kompilacji spakuje wtedy wszystkie pliki DEX do Twojego pliku APK.

W środowisku wykonawczym, zamiast wyszukiwać tylko w głównym pliku classes.dex, interfejsy API multidex używają specjalnego modułu ładowania klas do przeszukiwania wszystkich dostępnych plików DEX pod kątem Twoich metod.

Ograniczenia biblioteki multidex

Biblioteka multidex ma znane ograniczenia. Gdy uwzględniasz bibliotekę w 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 ANR (Application Not Responding), jeśli dodatkowe pliki DEX są duże. Aby uniknąć tego problemu, włącz zmniejszanie kodu, by zminimalizować rozmiar plików DEX i usunąć nieużywane fragmenty kodu.
  • Jeśli aplikacja działa na urządzeniach z Androidem starszym niż 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 całkowicie problemu.

    W wersjach starszych niż Android 4.0 możesz osiągnąć limit przydziału linearnego, zanim osiągniesz limit indeksu DEX. Jeśli kierujesz aplikację na poziomy interfejsu API niższe niż 14, przeprowadź szczegółowe testy na tych wersjach platformy, ponieważ aplikacja może mieć problemy przy uruchamianiu lub podczas wczytywania określonych grup klas.

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

Deklaruj klasy wymagane w podstawowym pliku DEX

Podczas tworzenia każdego pliku DEX na potrzeby aplikacji multidex narzędzia do kompilacji dokonują skomplikowanego procesu podejmowania decyzji, aby określić, które klasy są potrzebne w podstawowym pliku DEX, aby aplikacja mogła działać prawidłowo. Jeśli w podstawowym pliku DEX nie ma klasy wymaganej podczas uruchamiania, aplikacja ulega awarii i pojawia się błąd java.lang.NoClassDefFoundError.

Narzędzia do kompilacji rozpoznają ścieżki kodu dostępu do kodu bezpośrednio z kodu Twojej 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 wykorzystuje introspekcje lub wywoływanie metod Java z kodu natywnego, klasy te mogą nie zostać rozpoznane jako wymagane w głównym pliku DEX.

Jeśli otrzymujesz java.lang.NoClassDefFoundError, musisz ręcznie określić dodatkowe klasy wymagane w podstawowym pliku DEX, deklarując je w typie kompilacji za pomocą właściwości multiDexKeepProguard. Jeśli klasa jest zgodna w pliku multiDexKeepProguard, zostanie ona dodana do podstawowego 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 dostosowywaniu zawartości aplikacji znajdziesz w sekcji Dostosowywanie kodu do zachowania.

Plik wskazany w funkcji multiDexKeepProguard powinien zawierać opcje -keep w prawidłowej składni ProGuard. 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:

Odlotowy

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

Kotlin

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

Optymalizuj multidex w kompilacjach programistycznych

Konfiguracja multidexu wymaga znacznie dłuższego czasu przetwarzania kompilacji, ponieważ system kompilacji musi podejmować złożone decyzje o tym, które klasy powinny być uwzględnione w podstawowym pliku DEX, a które mogą być dołączane do dodatkowych plików DEX. Oznacza to, że kompilacje przyrostowe korzystające z multidexu zwykle trwają dłużej i mogą spowalniać proces programowania.

Aby ograniczyć dłuższe przyrostowe czasy kompilacji, użyj wstępnego dekodowania w celu ponownego wykorzystywania danych wyjściowych multidex między kompilacjami. Predeksowanie wykorzystuje format ART dostępny tylko na Androidzie 5.0 (poziom interfejsu API 21) i nowszych. Jeśli używasz Android Studio, IDE automatycznie korzysta z wstępnego dedeksowania podczas wdrażania aplikacji na urządzeniu z Androidem 5.0 (poziom interfejsu API 21) lub nowszym. Jeśli jednak uruchamiasz kompilacje Gradle z poziomu wiersza poleceń, musisz ustawić minSdkVersion na wartość 21 lub wyższą, aby umożliwić wstępne pliki.

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

Odlotowy

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 poznać więcej strategii przyspieszających kompilację w Android Studio lub za pomocą wiersza 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 przeznaczone do różnych potrzeb multidex, dla każdej wersji możesz podać inny plik manifestu. W takim przypadku tylko plik związany z interfejsem API na poziomie 20 lub niższym zmieni nazwę tagu <application>. Możesz też utworzyć inną podklasę Application dla każdego wariantu, więc tylko podklasa dla interfejsu API na poziomie 20 i niższym będzie rozszerzała klasę MultiDexApplication lub wywołania MultiDex.install(this).

Testowanie aplikacji multidex

Gdy piszesz testy z instrumentacją dla aplikacji multidex, nie musisz wprowadzać dodatkowej konfiguracji, jeśli używasz instrumentacji MonitoringInstrumentation lub AndroidJUnitRunner. Jeśli używasz innego obiektu 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);
  ...
}