Zmniejszanie, zaciemnianie i optymalizowanie aplikacji

Aby aplikacja była jak najmniejsza i jak najkrótsza, zoptymalizuj i zmniejsz kompilację wersji za pomocą isMinifyEnabled = true.

W ten sposób włącza się funkcja zmniejszania, która usuwa nieużywany kod i zasoby, zaciemnianie nazw klas i elementów aplikacji oraz optymalizacja, która stosuje bardziej agresywne strategie, aby jeszcze bardziej zmniejszyć rozmiar i poprawić wydajność aplikacji. Na tej stronie opisujemy, jak R8 wykonuje w projekcie zadania w czasie kompilacji oraz jak możesz je dostosować.

Jeśli tworzysz projekt z wykorzystaniem wtyczki Androida do obsługi Gradle w wersji 3.4.0 lub nowszej, wtyczka nie wykorzystuje już ProGuard do optymalizacji kodu w czasie kompilacji. Zamiast tego współpracuje z kompilatorem R8 w celu wykonywania następujących zadań kompilacji:

  • Zmniejszanie kodu (czyli drżenie drzew): wykrywa i bezpiecznie usuwa nieużywane klasy, pola, metody i atrybuty z aplikacji oraz jej zależności od bibliotek, dzięki czemu jest to przydatne narzędzie do obchodzenia limitu 64 tys. plików referencyjnych. Jeśli na przykład używasz tylko kilku interfejsów API zależności biblioteki, zmniejszanie może identyfikować kod biblioteki, z którego aplikacja nie korzysta, i usuwać z aplikacji tylko ten kod. Więcej informacji znajdziesz w sekcji poświęconej zmniejszaniu kodu.
  • Zmniejszanie zasobów: powoduje usunięcie nieużywanych zasobów z aplikacji w pakiecie, w tym nieużywanych zasobów z zależności biblioteki aplikacji. Działa w połączeniu z zmniejszaniem kodu, dzięki czemu po usunięciu nieużywanego kodu można bezpiecznie usunąć wszystkie zasoby, do których już się nie odwołujemy. Więcej informacji znajdziesz w sekcji poświęconej zmniejszaniu zasobów.
  • Optymalizacja: sprawdza i przepisuje kod, aby poprawić wydajność działania i jeszcze bardziej zmniejszyć rozmiar plików DEX aplikacji. Zwiększa to wydajność kodu w czasie działania nawet o 30%, znacznie usprawniając uruchamianie i czas wczytywania klatek. Jeśli na przykład R8 wykryje, że gałąź else {} w danej instrukcji if/else nigdy nie jest wykonywana, R8 usuwa kod gałęzi else {}. Więcej informacji znajdziesz w sekcji poświęconej optymalizacji kodu.
  • Zaciemnianie (lub minifikacja identyfikatora): skraca nazwy klas i elementów, co prowadzi do zmniejszenia rozmiaru plików DEX. Więcej informacji znajdziesz w sekcji o zaciemnianiu kodu.

Podczas kompilacji wersji aplikacji R8 można skonfigurować tak, aby wykonywał za Ciebie opisane powyżej zadania kompilacji. Możesz też wyłączyć niektóre zadania lub dostosować działanie R8 za pomocą plików reguł ProGuard. R8 działa ze wszystkimi istniejącymi plikami reguł ProGuard, więc zaktualizowanie wtyczki Androida do obsługi Gradle do wersji R8 nie powinno wymagać zmiany istniejących reguł.

Umożliwiaj zmniejszanie, zaciemnianie i optymalizację

Jeśli używasz Androida Studio 3.4 lub wtyczki Androida do obsługi Gradle w wersji 3.4.0 lub nowszej, R8 jest domyślnym kompilatorem, który konwertuje kod bajtowy Javy Twojego projektu na format DEX, który działa na platformie Androida. Jednak gdy tworzysz nowy projekt w Android Studio, zmniejszanie, zaciemnianie i optymalizacja kodu są domyślnie wyłączone. Dzieje się tak, ponieważ optymalizacje w czasie kompilacji wydłużają czas kompilacji i mogą spowodować błędy, jeśli nie dostosujesz odpowiednio kodu do zachowania.

Dlatego najlepiej jest włączyć zadania w czasie kompilacji podczas tworzenia ostatecznej wersji aplikacji, którą testujesz przed opublikowaniem. Aby włączyć zmniejszanie, zaciemnianie i optymalizację, w skrypcie kompilacji na poziomie projektu uwzględnij poniższe elementy.

Kotlin

android {
    buildTypes {
        getByName("release") {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type. Make sure to use a build
            // variant with `isDebuggable=false`.
            isMinifyEnabled = true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            isShrinkResources = true

            proguardFiles(
                // Includes the default ProGuard rules files that are packaged with
                // the Android Gradle plugin. To learn more, go to the section about
                // R8 configuration files.
                getDefaultProguardFile("proguard-android-optimize.txt"),

                // Includes a local, custom Proguard rules file
                "proguard-rules.pro"
            )
        }
    }
    ...
}

Odlotowy

android {
    buildTypes {
        release {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type. Make sure to use a build
            // variant with `debuggable false`.
            minifyEnabled true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            shrinkResources true

            // Includes the default ProGuard rules files that are packaged with
            // the Android Gradle plugin. To learn more, go to the section about
            // R8 configuration files.
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

Pliki konfiguracji R8

R8 używa plików reguł ProGuard do modyfikowania domyślnego działania i lepszego poznawania struktury aplikacji, np. klas, które służą jako punkty wejścia do kodu aplikacji. Część tych plików reguł można modyfikować, ale niektóre reguły mogą być generowane automatycznie przez narzędzia czasu kompilowania, takie jak AAPT2, lub dziedziczone z zależności biblioteki aplikacji. W tabeli poniżej opisujemy źródła plików reguł ProGuard wykorzystywanych w R8.

Źródło Lokalizacja Opis
Android Studio <module-dir>/proguard-rules.pro Gdy tworzysz nowy moduł w Android Studio, IDE tworzy plik proguard-rules.pro w katalogu głównym tego modułu.

Domyślnie ten plik nie podlega żadnym regułom. Uwzględnij tutaj własne reguły ProGuard, np. niestandardowe reguły przechowywania.

Wtyczka Androida do obsługi Gradle Wygenerowany przez wtyczkę Androida do obsługi Gradle podczas kompilacji. Wtyczka Androida do obsługi Gradle generuje plik proguard-android-optimize.txt, który zawiera reguły przydatne w większości projektów na Androida i włącza adnotacje @Keep*.

Domyślnie podczas tworzenia nowego modułu za pomocą Android Studio skrypt kompilacji na poziomie modułu zawiera ten plik reguł w Twojej kompilacji wersji.

Uwaga: wtyczka Androida do obsługi Gradle zawiera dodatkowe wstępnie zdefiniowane pliki reguł ProGuard, ale zalecamy używanie proguard-android-optimize.txt.

Zależności bibliotek Biblioteki AAR: <library-dir>/proguard.txt

Biblioteki JAR: <library-dir>/META-INF/proguard/

Jeśli biblioteka AAR została opublikowana z własnym plikiem reguł ProGuard i dodasz ją jako zależność czasu kompilowania, R8 automatycznie zastosuje jej reguły podczas kompilowania projektu.

Korzystanie z plików reguł połączonych z bibliotekami AAR jest przydatne, jeśli do poprawnego działania biblioteki wymagane są określone reguły przechowywania, czyli to, że programista biblioteki wykonał za Ciebie kroki rozwiązywania problemów.

Pamiętaj jednak, że reguły ProGuard są dodawane, dlatego nie można usunąć niektórych reguł, które obejmuje zależność biblioteki AAR, i mogą one mieć wpływ na kompilację innych części aplikacji. Jeśli na przykład biblioteka zawiera regułę wyłączania optymalizacji kodu, reguła ta wyłącza optymalizacje w całym projekcie.

Android Asset Package Tool 2 (AAPT2) Po utworzeniu projektu w usłudze minifyEnabled true: <module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt AAPT2 generuje reguły zachowywania na podstawie odwołań do klas w pliku manifestu, układach i innych zasobach aplikacji. Na przykład AAPT2 obejmuje regułę zachowywania dla każdej aktywności zarejestrowanej w pliku manifestu aplikacji jako punkt wejścia.
Niestandardowe pliki konfiguracji Domyślnie, gdy tworzysz nowy moduł przy użyciu Android Studio, IDE tworzy <module-dir>/proguard-rules.pro, aby umożliwić Ci dodanie własnych reguł. Możesz uwzględnić dodatkowe konfiguracje, a R8 zastosuje je podczas kompilacji.

Gdy ustawisz właściwość minifyEnabled na true, R8 połączy reguły ze wszystkich dostępnych źródeł wymienionych powyżej. Jest to ważne podczas rozwiązywania problemów z R8, ponieważ inne zależności czasu kompilacji, np. zależności bibliotek, mogą wprowadzić w działaniu R8 nieznane Ci zmiany.

Aby wygenerować pełny raport zawierający wszystkie reguły stosowane podczas tworzenia projektu, umieść w pliku proguard-rules.pro modułu ten kod:

// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt

Uwzględnij dodatkowe konfiguracje

Gdy tworzysz nowy projekt lub moduł w Android Studio, IDE tworzy plik <module-dir>/proguard-rules.pro, w którym możesz umieścić własne reguły. Dodatkowe reguły z innych plików możesz też dołączyć do właściwości proguardFiles w skrypcie kompilacji modułu.

Możesz np. dodać reguły odnoszące się do poszczególnych wariantów kompilacji, dodając kolejną właściwość proguardFiles w odpowiednim bloku productFlavor. Poniższy plik Gradle dodaje interfejs flavor2-rules.pro do charakteru produktu flavor2. Teraz flavor2 używa wszystkich 3 reguł ProGuard, ponieważ stosowane są również te z bloku release.

Możesz też dodać właściwość testProguardFiles, która określa listę plików ProGuard, które znajdują się tylko w testowym pakiecie APK:

Kotlin

android {
    ...
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                "proguard-rules.pro"
            )
            testProguardFiles(
                // The proguard files listed here are included in the
                // test APK only.
                "test-proguard-rules.pro"
            )
        }
    }
    flavorDimensions.add("version")
    productFlavors {
        create("flavor1") {
            ...
        }
        create("flavor2") {
            proguardFile("flavor2-rules.pro")
        }
    }
}

Odlotowy

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                'proguard-rules.pro'
            testProguardFiles
                // The proguard files listed here are included in the
                // test APK only.
                'test-proguard-rules.pro'
        }
    }
    flavorDimensions "version"
    productFlavors {
        flavor1 {
            ...
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

Zmniejszanie kodu

Zmniejszanie kodu za pomocą R8 jest domyślnie włączone, jeśli ustawisz właściwość minifyEnabled na true.

Zmniejszanie kodu (nazywane też potrząsaniem drzew) to proces usuwania kodu, który według R8 nie jest wymagany w czasie działania. Ten proces może znacznie zmniejszyć rozmiar aplikacji, jeśli np. aplikacja zawiera wiele zależności bibliotek, ale wykorzystuje tylko niewielką część ich funkcji.

Aby zmniejszyć kod aplikacji, R8 najpierw określa wszystkie punkty wejścia do kodu aplikacji na podstawie połączonego zbioru plików konfiguracji. Te punkty wejścia obejmują wszystkie klasy, których platforma Android może używać do otwierania usług lub działań w aplikacji. Począwszy od każdego punktu wejścia, R8 sprawdza kod aplikacji, aby utworzyć wykres wszystkich metod, zmiennych członkowskich i innych klas, do których aplikacja może mieć dostęp w czasie działania. Kod, który nie jest połączony z tym wykresem, jest uważany za nieosiągalny i może zostać usunięty z aplikacji.

Rysunek 1 przedstawia aplikację z zależnością biblioteki środowiska wykonawczego. Podczas sprawdzania kodu aplikacji R8 ustala, że metody foo(), faz() i bar() są dostępne z punktu wejścia MainActivity.class. Jednak klasa OkayApi.class lub jej metoda baz() nigdy nie są używane przez aplikację w czasie działania, a R8 usuwa ten kod, gdy aplikacja się zmniejsza.

Rysunek 1. W czasie kompilacji R8 buduje wykres na podstawie połączonych reguł przechowywania Twojego projektu, aby określić nieosiągalny kod.

R8 określa punkty wejścia za pomocą reguł -keep w plikach konfiguracji R8 projektu. Oznacza to, że reguły określają klasy, których R8 nie powinna odrzucać przy zmniejszeniu aplikacji. R8 traktuje te klasy jako możliwe punkty wejścia do aplikacji. Wtyczka Androida do obsługi Gradle i AAPT2 automatycznie generują reguły przechowywania wymagane przez większość projektów aplikacji, takie jak działania, wyświetlenia i usługi w aplikacji. Jeśli jednak chcesz dostosować to domyślne działanie za pomocą dodatkowych reguł Keep, przeczytaj sekcję o dostosowywaniu kodu do zachowania.

Jeśli zamiast tego chcesz zmniejszyć rozmiar zasobów aplikacji, przejdź do sekcji z informacjami o zmniejszaniu zasobów.

Określ, który kod chcesz zachować

W większości sytuacji do usunięcia nieużywanego kodu wystarczy domyślny plik reguł ProGuard (proguard-android- optimize.txt). Niektóre sytuacje są jednak trudne do prawidłowej analizy i mogą spowodować usunięcie kodu, którego aplikacja faktycznie potrzebuje. Oto kilka przykładów nieprawidłowego usunięcia kodu:

  • gdy aplikacja wywołuje metodę z interfejsu natywnego interfejsu Java (JNI).
  • Gdy aplikacja wyszukuje kod w czasie działania (np. z odbiciem)

Testowanie aplikacji powinno ujawnić wszystkie błędy wynikające z nieprawidłowego usunięcia kodu. Aby sprawdzić, jaki kod został usunięty, możesz też wygenerować raport o usuniętym kodzie.

Aby naprawić błędy i wymusić zachowanie kodu R8, dodaj wiersz -keep w pliku reguł ProGuard. Na przykład:

-keep public class MyClass

Do kodu, który chcesz zachować, możesz też dodać adnotację @Keep. Dodanie @Keep do klasy zachowuje całą klasę w niezmienionej postaci. Dodanie go do metody lub pola sprawi, że metoda/pole (i jego nazwa) oraz jej nazwa pozostaną nienaruszone. Pamiętaj, że ta adnotacja jest dostępna tylko wtedy, gdy używasz biblioteki adnotacji na AndroidaX i dołączysz plik reguł ProGuard, który jest dołączony do wtyczki Androida Gradle, zgodnie z opisem w sekcji dotyczącej włączania zmniejszania.

Używając opcji -keep, należy wziąć pod uwagę wiele kwestii. Więcej informacji na temat dostosowywania pliku reguł znajdziesz w instrukcji ProGuard. W sekcji Rozwiązywanie problemów opisujemy inne częste problemy, które mogą wystąpić po schnięciu kodu.

Usuń biblioteki natywne

Domyślnie biblioteki kodu natywnego są usuwane z kompilacji Twojej aplikacji. Obejmuje to usunięcie tabeli symboli i informacji debugowania znajdujących się w bibliotekach natywnych używanych przez Twoją aplikację. Usunięcie bibliotek kodu natywnego pozwala znacznie zmniejszyć rozmiar, jednak z powodu brakujących informacji (takich jak nazwy klas i funkcji) nie można zdiagnozować w Konsoli Google Play.

Pomoc w przypadku awarii natywnych

Konsola Google Play zgłasza awarie natywne w Android Vitals. Wystarczy kilka czynności, aby wygenerować i przesłać natywny plik z symbolami debugowania w przypadku Twojej aplikacji. Ten plik umożliwia korzystanie w Android Vitals z symbolizowanego zrzutu stosu awarii (zawierających nazwy klas i funkcji) ułatwiających debugowanie aplikacji w środowisku produkcyjnym. Te kroki różnią się w zależności od wersji wtyczki Androida do obsługi Gradle używanej w projekcie i wyników kompilacji.

Wtyczka Androida do obsługi Gradle w wersji 4.1 lub nowszej

Jeśli w projekcie tworzysz pakiet Android App Bundle, możesz automatycznie uwzględnić w nim plik natywnych symboli debugowania. Aby uwzględnić ten plik w kompilacjach wersji, do pliku build.gradle.kts aplikacji dodaj ten ciąg znaków:

android.buildTypes.release.ndk.debugSymbolLevel = { SYMBOL_TABLE | FULL }

Wybierz poziom symbolu debugowania z tych opcji:

  • SYMBOL_TABLE umożliwia uzyskiwanie nazw funkcji w symbolicznych zrzutach stosu w Konsoli Play. Ten poziom obsługuje tombstone.
  • Użyj metody FULL, aby uzyskiwać nazwy funkcji, pliki i numery wierszy w symbolicznych zrzutach stosu w Konsoli Play.

Jeśli w ramach projektu tworzysz plik APK, użyj pokazanego wcześniej ustawienia kompilacji build.gradle.kts, aby oddzielnie wygenerować plik z symbolami na potrzeby debugowania kodu natywnego. Ręcznie prześlij plik symboli do debugowania kodu natywnego do Konsoli Google Play. W ramach procesu kompilacji wtyczka Androida do obsługi Gradle generuje ten plik w tej lokalizacji projektu:

app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip

Wtyczka Androida do obsługi Gradle w wersji 4.0 lub starszej (i inne systemy kompilacji)

W ramach procesu kompilacji wtyczka Androida do obsługi Gradle przechowuje w katalogu projektu kopię nieskróconych bibliotek. Ta struktura katalogów jest podobna do tej:

app/build/intermediates/cmake/universal/release/obj/
├── armeabi-v7a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── arm64-v8a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── x86/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
└── x86_64/
    ├── libgameengine.so
    ├── libothercode.so
    └── libvideocodec.so
  1. Skompresuj zawartość tego katalogu:

    cd app/build/intermediates/cmake/universal/release/obj
    zip -r symbols.zip .
    
  2. Ręcznie prześlij plik symbols.zip do Konsoli Google Play.

Zmniejszanie zasobów

Zmniejszanie zasobów działa tylko w połączeniu z zmniejszaniem kodu. Gdy skrót kodu usunie cały nieużywany kod, może on określić, które zasoby wciąż zużywa aplikacja. Jest to szczególnie zauważalne w przypadku dodawania bibliotek kodu zawierających zasoby. Musisz usunąć nieużywany kod biblioteki, aby zasoby tej biblioteki nie były odsyłane, a tym samym można je było usunąć przez reduktor zasobów.

Aby włączyć zmniejszanie zasobów, ustaw w skrypcie kompilacji właściwość shrinkResources na true (obok minifyEnabled w celu zmniejszania kodu). Na przykład:

Kotlin

android {
    ...
    buildTypes {
        getByName("release") {
            isShrinkResources = true
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Odlotowy

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android.txt'),
                'proguard-rules.pro'
        }
    }
}

Jeśli Twoja aplikacja nie została jeszcze utworzona za pomocą minifyEnabled w celu zmniejszenia kodu, spróbuj to zrobić, zanim włączysz shrinkResources. Być może trzeba będzie zmodyfikować plik proguard-rules.pro, aby zachować klasy lub metody tworzone lub wywoływane dynamicznie, zanim zaczniesz usuwać zasoby.

Określanie, które zasoby mają być przechowywane

Jeśli chcesz zachować lub odrzucić określone zasoby, utwórz w projekcie plik XML z tagiem <resources> i w atrybucie tools:keep wskaż każdy zasób do zachowania, a każdy zasób do odrzucenia w atrybucie tools:discard. Oba atrybuty akceptują oddzieloną przecinkami listę nazw zasobów. Możesz użyć symbolu gwiazdki jako symbolu wieloznacznego.

Na przykład:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

Zapisz ten plik w zasobach projektu, na przykład pod adresem res/raw/keep.xml. Kompilacja nie umieszcza tego pliku w pakiecie aplikacji.

Określanie, które zasoby mają być odrzucane, może wydawać się banalne, ale w razie potrzeby usuwać je, ale może to być przydatne, jeśli korzystasz z wariantów kompilacji. Możesz na przykład umieścić wszystkie zasoby we wspólnym katalogu projektów, a potem utworzyć osobny plik keep.xml dla każdego wariantu kompilacji, gdy wiesz, że dany zasób jest używany w kodzie (i tym samym nie jest usuwany przez skracanie), ale w rzeczywistości nie będzie używany w przypadku danego wariantu kompilacji. Może się też zdarzyć, że narzędzia do kompilacji nieprawidłowo zidentyfikowały zasób jako potrzebny. Jest to możliwe, ponieważ kompilator dodaje identyfikatory zasobów w tekście, a analizator zasobów może nie znać różnicy między rzeczywiście przywoływanym zasobem a liczbą całkowitą w kodzie, która ma taką samą wartość.

Włącz rygorystyczne sprawdzanie odwołań

Zwykle ograniczający zasoby może dokładnie określić, czy zasób jest używany. Jeśli jednak Twój kod wywołuje metodę Resources.getIdentifier() (lub jeśli któraś z Twoich bibliotek to robi – biblioteka AppCompat), oznacza to, że wyszukuje on nazwy zasobów na podstawie ciągów generowanych dynamicznie. Gdy to zrobisz, shrinker będzie domyślnie działać w sposób defensywny i oznaczy wszystkie zasoby o pasującym formacie nazwy jako potencjalnie używane i niedostępne do usunięcia.

Na przykład ten kod powoduje, że wszystkie zasoby z prefiksem img_ są oznaczane jako używane.

Kotlin

val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)

Java

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

Ograniczanie zasobów przegląda również wszystkie stałe w kodzie i różne zasoby res/raw/ w poszukiwaniu adresów URL zasobów w formacie podobnym do file:///android_res/drawable//ic_plus_anim_016.png. Jeśli znajdzie takie ciągi znaków, które wyglądają jak ten, które mogą posłużyć do tworzenia adresów URL tego typu, nie usunie ich.

Oto przykłady bezpiecznego trybu zmniejszania, który jest domyślnie włączony. Możesz jednak wyłączyć obsługę takiego ustawienia i określić, że w ograniczaniu zasobów będą używane tylko te zasoby, które na pewno są pewne. Aby to zrobić, ustaw shrinkMode na strict w pliku keep.xml w następujący sposób:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

Jeśli włączysz tryb ścisłego ograniczania, a Twój kod odwołuje się również do zasobów z ciągami generowanymi dynamicznie (jak pokazano powyżej), musisz zachować te zasoby ręcznie za pomocą atrybutu tools:keep.

Usuń nieużywane zasoby alternatywne

Zmniejszający zasoby Gradle usuwa tylko te zasoby, do których nie odwołuje się kod aplikacji. Oznacza to, że nie usuwa zasobów alternatywnych dla różnych konfiguracji urządzeń. W razie potrzeby możesz użyć właściwości resConfigs wtyczki Androida do obsługi Gradle, aby usunąć alternatywne pliki zasobów, których aplikacja nie potrzebuje.

Jeśli np. używasz biblioteki, która zawiera zasoby językowe (np. AppCompat lub Usługi Google Play), aplikacja zawiera wszystkie przetłumaczone ciągi tekstowe wiadomości w tych bibliotekach, niezależnie od tego, czy reszta aplikacji jest przetłumaczona na te same języki. Jeśli chcesz zachować tylko te języki, które są oficjalnie obsługiwane przez Twoją aplikację, możesz je określić za pomocą właściwości resConfig. Wszystkie zasoby dla niewymienionych języków zostaną usunięte.

Ten fragment kodu pokazuje, jak ograniczyć zasoby językowe tylko do języka angielskiego i francuskiego:

Kotlin

android {
    defaultConfig {
        ...
        resourceConfigurations.addAll(listOf("en", "fr"))
    }
}

Odlotowy

android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}

Gdy aplikacja jest publikowana w formacie Android App Bundle, domyślnie pobierane są tylko języki skonfigurowane na urządzeniu użytkownika. Analogicznie pobierane są tylko zasoby pasujące do gęstości ekranu urządzenia i biblioteki natywne pasujące do interfejsu ABI urządzenia. Więcej informacji znajdziesz w artykule o konfiguracji pakietu Android App Bundle.

W przypadku starszych aplikacji udostępnianych z plikami APK (utworzonych przed sierpniem 2021 r.) możesz dostosować gęstość ekranu lub zasoby ABI, które chcesz uwzględnić w pliku APK, tworząc wiele plików APK, z których każdy będzie przeznaczony na inną konfigurację urządzenia.

Scalanie zduplikowanych zasobów

Domyślnie Gradle scala też zasoby o jednakowych nazwach, takie jak elementy rysunkowe o tej samej nazwie, które mogą znajdować się w różnych folderach zasobów. Tym działaniem nie steruje właściwość shrinkResources i nie można go wyłączyć, ponieważ trzeba uniknąć błędów, gdy wiele zasobów pasuje do nazwy wyszukanej przez kod.

Scalanie zasobów następuje tylko wtedy, gdy co najmniej 2 pliki mają identyczną nazwę, typ i kwalifikator zasobu. Gradle wybiera plik, który uważa za najlepszy spośród duplikatów (na podstawie kolejności priorytetów opisanej poniżej), i przekazuje tylko 1 zasób do AAPT w celu dystrybucji w końcowym artefakcie.

Gradle szuka zduplikowanych zasobów w tych lokalizacjach:

  • Główne zasoby, powiązane z głównym zbiorem źródłowym, zwykle znajdują się w lokalizacji src/main/res/.
  • Nakładki wariantów z typu kompilacji i smaków kompilacji.
  • Zależności projektu biblioteki.

Gradle scala zduplikowane zasoby w następującej kolejności priorytetowej:

Zależności → Główna → Typ kompilacji → Typ kompilacji

Jeśli na przykład zduplikowany zasób pojawia się zarówno w zasobach głównych, jak i rodzaju kompilacji, Gradle wybiera ten w rodzaju kompilacji.

Jeśli w tym samym zbiorze źródłowym znajdują się identyczne zasoby, Gradle nie może ich scalić i wystąpi błąd scalania zasobów. Może się tak zdarzyć, jeśli zdefiniujesz wiele zbiorów źródłowych we właściwości sourceSet w pliku build.gradle.kts – np. gdy obie komponenty src/main/res/ i src/main/res2/ zawierają identyczne zasoby.

Zaciemnianie kodu

Celem zaciemniania kodu jest zmniejszenie rozmiaru aplikacji przez skrócenie nazw klas, metod i pól aplikacji. Oto przykład zaciemnienia kodu za pomocą R8:

androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
    android.content.Context mContext -> a
    int mListItemLayout -> O
    int mViewSpacingRight -> l
    android.widget.Button mButtonNeutral -> w
    int mMultiChoiceItemLayout -> M
    boolean mShowTitle -> P
    int mViewSpacingLeft -> j
    int mButtonPanelSideLayout -> K

Chociaż zaciemnianie kodu nie powoduje usunięcia kodu z aplikacji, można zauważyć znaczne zmniejszenie rozmiaru aplikacji z plikami DEX, które indeksują wiele klas, metod i pól. Ponieważ jednak zaciemnianie kodu zmienia nazwy różnych części kodu, niektóre zadania, takie jak sprawdzanie zrzutów stosu, wymagają dodatkowych narzędzi. Aby poznać ślad stosu po zaciemnieniu kodu, przeczytaj sekcję o dekodowaniu zaciemnionego zrzutu stosu.

Poza tym, jeśli kod opiera się na przewidywalnych nazwach metod i klas aplikacji, na przykład podczas korzystania z odbicia należy traktować te podpisy jako punkty wejścia i określać reguły ich zachowania zgodnie z opisem w sekcji dotyczącej dostosowywania kodu do zachowywania. Zgodnie z tymi regułami R8 nie tylko zachowuje kod w końcowym pliku DEX, ale także zachowuje pierwotną nazwę.

Dekodowanie zaciemnionego zrzutu stosu

Gdy R8 zaciemnia kod, zrozumienie zrzutu stosu może być trudne (a nawet niemożliwe), ponieważ nazwy klas i metod mogły ulec zmianie. Aby uzyskać pierwotny zrzut stosu, musisz ponownie przeprowadzić jego śledzenie.

Optymalizacja kodu

Aby jeszcze bardziej zoptymalizować aplikację, R8 sprawdza kod na głębszym poziomie, usuwając więcej nieużywanego kodu. W miarę możliwości możesz zmodyfikować go tak, aby był mniej szczegółowy. Oto kilka przykładów takich optymalizacji:

  • Jeśli Twój kod nigdy nie pobiera gałęzi else {} dla danej instrukcji if/else, R8 może usunąć kod gałęzi else {}.
  • Jeśli Twój kod wywołuje metodę tylko w kilku miejscach, R8 może ją usunąć i umieścić w kilku miejscach.
  • Jeśli R8 ustali, że klasa ma tylko 1 unikalną podklasę, a sama klasa nie jest utworzona (np. abstrakcyjna klasa bazowa używana tylko przez jedną konkretną klasę implementacji), R8 może połączyć te 2 klasy i usunąć jedną z aplikacji.
  • Aby dowiedzieć się więcej, przeczytaj posty na blogu Jake'a Whartona o optymalizacji R8.

R8 nie pozwala na wyłączenie lub włączenie dyskretnych optymalizacji ani na zmianę działania optymalizacji. R8 ignoruje wszystkie reguły ProGuard, które próbują modyfikować domyślne optymalizacje, np. -optimizations i -optimizationpasses. To ograniczenie jest ważne, bo ponieważ wciąż ulepszamy R8, utrzymanie standardowego sposobu optymalizacji ułatwia zespołowi Android Studio rozwiązywanie potencjalnych problemów.

Pamiętaj, że włączenie optymalizacji zmieni zrzuty stosu Twojej aplikacji. Na przykład wstawienie spowoduje usunięcie ramek stosów. Informacje o tym, jak uzyskać oryginalne zrzuty stosu, znajdziesz w sekcji o wycofywaniu.

Wpływ na wydajność działania

Jeśli włączone jest zmniejszanie, zaciemnianie i optymalizacja, R8 poprawia wydajność działania kodu (w tym czas uruchamiania i klatki w wątku UI) nawet o 30%. Wyłączenie dowolnej z tych opcji znacznie ogranicza zestaw optymalizacji używanych przez R8.

Jeśli włączono R8, warto też utworzyć profile uruchamiania, aby uzyskać jeszcze lepszą wydajność uruchamiania.

Włącz bardziej agresywne optymalizacje

R8 zawiera zestaw dodatkowych optymalizacji (nazywanych „trybem pełnego”), które sprawiają, że jego działanie różni się od działania ProGuard. Te optymalizacje są domyślnie włączone od wtyczki do Androida do obsługi Gradle w wersji 8.0.0.

Aby wyłączyć te dodatkowe optymalizacje, wstaw te elementy w pliku gradle.properties projektu:

android.enableR8.fullMode=false

Dodatkowe optymalizacje powodują, że R8 zachowuje się inaczej niż ProGuard, więc mogą wymagać zastosowania dodatkowych reguł ProGuard, aby uniknąć problemów ze środowiskiem wykonawczym, jeśli używasz reguł przeznaczonych dla ProGuard. Załóżmy na przykład, że Twój kod odwołuje się do klasy za pomocą interfejsu Java Reflection API. Gdy nie używasz „trybu pełnego”, R8 zakłada, że zamierzasz badać i modyfikować obiekty tej klasy w czasie działania – nawet jeśli Twój kod w rzeczywistości tego nie robi – i automatycznie zachowuje klasę i swój statyczny inicjator.

Gdy jednak używasz „trybu całego ruchu”, R8 nie przyjmuje tego założenia i jeśli R8 twierdzi, że Twój kod nigdy nie używa klasy w czasie działania, usuwa ją z końcowego pliku DEX aplikacji. Oznacza to, że jeśli chcesz zachować klasę i jej statyczny inicjator, musisz umieścić w pliku reguł regułę przechowywania, aby to zrobić.

Jeśli napotkasz problemy podczas korzystania z trybu pełnego R8, poszukaj rozwiązania na stronie z najczęstszymi pytaniami na R8. Jeśli nie możesz rozwiązać problemu, zgłoś błąd.

Wycofuję zrzuty stosu

Kod przetwarzany przez R8 jest zmieniany na różne sposoby, co może utrudniać zrozumienie zrzutów stosu, ponieważ nie będą dokładnie odpowiadać kodowi źródłowemu. Zdarza się to w przypadku zmiany numerów wierszy, gdy informacje na potrzeby debugowania nie są zachowywane. Może to być spowodowane optymalizacją, takim jak wbudowanie lub kontur. Największym czynnikiem jest zaciemnianie kodu – nawet klasy i metody zmieniają nazwy.

Aby przywrócić pierwotny zrzut stosu, R8 udostępnia narzędzie wiersza poleceń retrace, które jest dołączone do pakietu narzędzi wiersza poleceń.

Aby umożliwić ponowne śledzenie zrzutów stosu swojej aplikacji, sprawdź, czy kompilacja zawiera wystarczającą ilość informacji, by się z nią przypominać. Aby to zrobić, dodaj te reguły do pliku proguard-rules.pro modułu:

-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

Atrybut LineNumberTable zawiera informacje o pozycji w metodach, dzięki którym dane o pozycji są drukowane w zrzucie stosu. Atrybut SourceFile sprawia, że wszystkie potencjalne środowiska wykonawcze rzeczywiście wyświetlają informacje pozycjonujące. Dyrektywa -renamesourcefileattribute ustawia nazwę pliku źródłowego w zapisach stosu na wartość SourceFile. Rzeczywista nazwa oryginalnego pliku źródłowego nie jest wymagana przy pobieraniu, ponieważ plik mapowania zawiera oryginalny plik źródłowy.

Przy każdym uruchomieniu R8 tworzy plik mapping.txt, który zawiera informacje potrzebne do mapowania zrzutów stosu do oryginalnych logów czasu. Android Studio zapisze plik w katalogu <module-name>/build/outputs/mapping/<build-type>/.

Gdy publikujesz aplikację w Google Play, możesz przesłać plik mapping.txt dla każdej z nich. W przypadku publikowania za pomocą pakietów Android App Bundle ten plik jest automatycznie dodawany do treści pakietów aplikacji. Następnie Google Play będzie ponownie śledzić przychodzące zrzuty stosu z problemów zgłoszonych przez użytkowników, aby umożliwić Ci ich sprawdzanie w Konsoli Play. Więcej informacji znajdziesz w artykule w Centrum pomocy usuwania zaciemnienia zrzutów stosu awarii.

Rozwiązywanie problemów przy użyciu R8

W tej sekcji omawiamy kilka strategii rozwiązywania problemów związanych z włączaniem zmniejszania, zaciemniania i optymalizacji za pomocą R8. Jeśli nie znajdziesz poniżej rozwiązania problemu, przeczytaj też stronę z najczęstszymi pytaniami na R8 oraz przewodnik rozwiązywania problemów w ProGuard.

Generowanie raportu o usuniętym (lub zachowanym) kodzie

Aby rozwiązać niektóre problemy z R8, przyda Ci się raport pokazujący cały kod usunięty z Twojej aplikacji przez R8. W przypadku każdego modułu, dla którego chcesz wygenerować ten raport, dodaj -printusage <output-dir>/usage.txt do pliku z regułami niestandardowymi. Gdy włączysz R8 i stworzysz aplikację, R8 wygeneruje raport z podaną ścieżką i nazwą pliku. Raport o usuniętym kodzie wygląda podobnie do tego:

androidx.drawerlayout.R$attr
androidx.vectordrawable.R
androidx.appcompat.app.AppCompatDelegateImpl
    public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
    public boolean hasWindowFeature(int)
    public void setHandleNativeActionModesEnabled(boolean)
    android.view.ViewGroup getSubDecor()
    public void setLocalNightMode(int)
    final androidx.appcompat.app.AppCompatDelegateImpl$AutoNightModeManager getAutoNightModeManager()
    public final androidx.appcompat.app.ActionBarDrawerToggle$Delegate getDrawerToggleDelegate()
    private static final boolean DEBUG
    private static final java.lang.String KEY_LOCAL_NIGHT_MODE
    static final java.lang.String EXCEPTION_HANDLER_MESSAGE_SUFFIX
...

Jeśli zamiast tego chcesz zobaczyć raport z punktami wejścia, które R8 określa na podstawie reguł przechowywania w Twoim projekcie, umieść -printseeds <output-dir>/seeds.txt w pliku z regułami niestandardowymi. Gdy włączysz R8 i stworzysz aplikację, R8 wygeneruje raport z podaną ścieżką i nazwą pliku. Raport o zachowanych punktach wejścia wygląda podobnie do tego:

com.example.myapplication.MainActivity
androidx.appcompat.R$layout: int abc_action_menu_item_layout
androidx.appcompat.R$attr: int activityChooserViewStyle
androidx.appcompat.R$styleable: int MenuItem_android_id
androidx.appcompat.R$styleable: int[] CoordinatorLayout_Layout
androidx.lifecycle.FullLifecycleObserverAdapter
...

Rozwiązywanie problemów z zmniejszaniem zasobów

Gdy zmniejszasz zasoby, w oknie Kompilacji wyświetla się podsumowanie zasobów usuniętych z aplikacji. (Aby wyświetlić szczegółowy tekst wyjściowy z Gradle, musisz najpierw kliknąć Przełącz widok po lewej stronie). Na przykład:

:android:shrinkDebugResources
Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning

Gradle tworzy też plik diagnostyczny o nazwie resources.txt w <module-name>/build/outputs/mapping/release/ (tym samym folderze co pliki wyjściowe ProGuard). Ten plik zawiera informacje takie jak to, które zasoby odwołują się do innych zasobów oraz które zasoby są używane lub usuwane.

Aby np. dowiedzieć się, dlaczego w aplikacji nadal występuje @drawable/ic_plus_anim_016, otwórz plik resources.txt i wyszukaj jego nazwę. Może się okazać, że odwołanie do niego pochodzi z innego zasobu:

16:25:48.005 [QUIET] [system.out] &#64;drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]     &#64;drawable/ic_plus_anim_016

Musisz teraz dowiedzieć się, dlaczego zasób @drawable/add_schedule_fab_icon_anim jest osiągalny. Jeśli przeszukasz trochę w górę, zobaczysz, że zasób jest wymieniony w sekcji „Główne zasoby osiągalne to:”. Oznacza to, że istnieje odwołanie do kodu add_schedule_fab_icon_anim (czyli jego identyfikator R.drawable ID został znaleziony w osiągalnym kodzie).

Jeśli nie używasz ścisłej kontroli, identyfikatory zasobów możesz oznaczyć jako osiągalne, o ile istnieją stałe ciągi znaków, które wyglądają, jakby można było je wykorzystać do tworzenia nazw zasobów dla zasobów wczytywanych dynamicznie. W takim przypadku, jeśli wyszukasz w danych wyjściowych kompilacji nazwę zasobu, może pojawić się ten komunikat:

10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
    used because it format-string matches string pool constant ic_plus_anim_%1$d.

Jeśli zobaczysz jeden z tych ciągów znaków i masz pewność, że ciąg znaków nie jest używany do dynamicznego wczytywania danego zasobu, możesz użyć atrybutu tools:discard, aby poinformować system kompilacji o jego usunięciu. Instrukcje znajdziesz w sekcji poświęconej dostosowywaniu zasobów do zachowania.