Zmniejszanie, zaciemnianie i optymalizowanie aplikacji

Aby aplikacja była jak najmniejsza i szybka, zoptymalizuj i zminifikuj kompilację wersji za pomocą isMinifyEnabled = true.

Spowoduje to ograniczenie, które skutkuje usunięciem nieużywanego kodu i zasobów, zaciemnianiem, które skraca nazwy klas i członków aplikacji, oraz optymalizacji, która stosuje bardziej agresywne strategie w celu dalszego zmniejszenia rozmiaru i poprawy wydajności aplikacji. Na tej stronie opisujemy, jak R8 wykonuje w projekcie zadania związane z kompilacją oraz jak można je dostosować.

Gdy tworzysz projekt z użyciem wtyczki Androida do obsługi Gradle w wersji 3.4.0 lub nowszej, wtyczka nie używa już ProGuard do optymalizacji kodu w czasie kompilacji. Zamiast tego wtyczka współpracuje z kompilatorem R8, aby wykonywać te zadania podczas kompilacji:

  • Zmniejszanie kodu (lub drżenie drzewem): wykrywa i bezpiecznie usuwa nieużywane klasy, pola, metody i atrybuty z aplikacji oraz jej bibliotek. Dzięki temu jest to przydatne narzędzie do obchodzenia limitu plików referencyjnych, który wynosi 64 tys. Jeśli na przykład używasz tylko kilku interfejsów API w zależności od biblioteki, zmniejszanie może wskazać kod biblioteki, którego aplikacja nie używa, i usunąć tylko ten kod z aplikacji. Więcej informacji znajdziesz w sekcji o zmniejszaniu kodu.
  • Zmniejszanie zasobów: usuwa nieużywane zasoby z aplikacji w pakiecie, w tym te nieużywane, które są uwzględnione w zależnościach biblioteki aplikacji. Działa w połączeniu ze zmniejszaniem kodu, dzięki czemu po usunięciu nieużywanego kodu można bezpiecznie usunąć wszystkie zasoby, do których już się nie odwołują. Więcej informacji znajdziesz w sekcji o zmniejszaniu zasobów.
  • Optymalizacja: sprawdza i przepisuje kod, aby poprawić wydajność środowiska wykonawczego i jeszcze bardziej zmniejszyć rozmiar plików DEX Twojej aplikacji. Zwiększa to wydajność kodu w czasie działania aplikacji nawet o 30%, znacząco poprawiając czas uruchamiania i uruchamiania klatek. Jeśli na przykład R8 wykryje, że gałąź else {} dla danej instrukcji if/else 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 członków, co skutkuje mniejszym rozmiarem plików DEX. Więcej informacji znajdziesz w sekcji o zaciemnianiu kodu.

Podczas tworzenia wersji produkcyjnej aplikacji R8 można skonfigurować tak, aby wykonywał opisane powyżej zadania związane z kompilacją. 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 zmiana wtyczki Androida do obsługi Gradle, tak aby korzystała z R8, nie powinna wymagać zmiany istniejących reguł.

Włącz 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, domyślnym kompilatorem jest R8, który konwertuje kod bajtowy projektu w Javie na format DEX, który działa na platformie Androida. Jednak w przypadku tworzenia nowego projektu w Android Studio zmniejszanie, zaciemnianie i optymalizacja kodu są domyślnie wyłączone. Wynika to z tego, że optymalizacje podczas kompilowania wydłużają czas kompilacji projektu i mogą powodować błędy, jeśli nie dostosujesz kodu do zachowania w wystarczającym stopniu.

Dlatego najlepiej jest włączyć te zadania podczas kompilacji podczas tworzenia ostatecznej wersji aplikacji, którą przetestujesz przed opublikowaniem. Aby umożliwić 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"
            )
        }
    }
    ...
}

Odlotowe

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 jej kodu. Chociaż możesz modyfikować niektóre z tych plików reguł, niektóre z nich mogą być generowane automatycznie przez narzędzia do kompilowania, takie jak AAPT2, lub odziedziczone z zależności biblioteki aplikacji. W tabeli poniżej znajdziesz źródła plików reguł ProGuard, z których korzysta R8.

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

Domyślnie ten plik nie stosuje żadnych reguł. Dołącz więc tutaj własne reguły ProGuard, takie jak niestandardowe reguły Keep.

Wtyczka Androida do obsługi Gradle Wygenerowany przez wtyczkę Androida do obsługi Gradle podczas kompilowania. Wtyczka Androida do obsługi Gradle generuje zadanie proguard-android-optimize.txt, które zawiera reguły przydatne w większości projektów Androida i umożliwia włączenie adnotacji @Keep*.

Domyślnie podczas tworzenia nowego modułu w Android Studio skrypt kompilacji na poziomie modułu dołącza ten plik reguł do kompilacji wersji.

Uwaga: wtyczka Androida do obsługi Gradle zawiera dodatkowe wstępnie zdefiniowane pliki reguł ProGuard, ale zalecamy użycie 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 dołączysz ten AAR jako zależność czasu kompilowania, R8 automatycznie stosuje swoje reguły podczas kompilowania projektu.

Korzystanie z plików reguł zawartych w bibliotekach AAR jest przydatne, gdy do prawidłowego działania biblioteki wymagane są określone reguły przechowywania (czyli to jej deweloper wykonał czynności niezbędne do rozwiązania problemu za Ciebie).

Należy jednak pamiętać, że ponieważ reguły ProGuard są dodatkowe, nie można usunąć niektórych reguł uwzględnionych w zależności biblioteki AAR. Może to 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 minifyEnabled true: <module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt AAPT2 generuje reguły przechowywania na podstawie odwołań do klas w pliku manifestu, układach i innych zasobach aplikacji. Na przykład AAPT2 zawiera regułę przechowywania dla każdej aktywności, którą zarejestrujesz w pliku manifestu aplikacji jako punkt wejścia.
Niestandardowe pliki konfiguracji Domyślnie, gdy tworzysz nowy moduł w Android Studio, IDE tworzy <module-dir>/proguard-rules.pro, aby umożliwić Ci dodanie własnych reguł. Możesz uwzględnić dodatkowe konfiguracje, które R8 zastosuje je podczas kompilowania.

Jeśli ustawisz właściwość minifyEnabled na wartość true, R8 będzie łączyć reguły ze wszystkich dostępnych źródeł wymienionych powyżej. Należy o tym pamiętać podczas rozwiązywania problemów z R8, ponieważ inne zależności czasu kompilacji, takie jak zależności bibliotek, mogą wprowadzić zmiany w działaniu R8, których nie znasz.

Aby wygenerować pełny raport ze wszystkimi regułami, których podlega R8 podczas tworzenia projektu, w pliku proguard-rules.pro modułu umieść te dane:

// 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. Możesz też uwzględnić dodatkowe reguły z innych plików, dodając je we właściwości proguardFiles w skrypcie kompilacji modułu.

Możesz na przykład dodać reguły odnoszące się do poszczególnych wariantów kompilacji, dodając kolejną właściwość proguardFiles w odpowiednim bloku productFlavor. Ten plik Gradle dodaje atrybut flavor2-rules.pro do rodzaju produktów flavor2. Teraz flavor2 używa wszystkich 3 reguł ProGuard, ponieważ stosowane są również reguły z blokady release.

Możesz też dodać właściwość testProguardFiles, która określa listę plików ProGuard znajdujących 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")
        }
    }
}

Odlotowe

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 po ustawieniu właściwości 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 na przykład aplikacja zawiera wiele zależności od bibliotek, ale wykorzystuje tylko niewielką część ich funkcji.

Aby zmniejszyć kod aplikacji, R8 najpierw określa wszystkie punkty wejścia w kodzie aplikacji na podstawie połączonego zestawu plików konfiguracji. Te punkty wejścia obejmują wszystkie klasy, których platforma Androida może używać do otwierania aktywności lub usług w aplikacji. Poczynając 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ę, która jest uzależniona od biblioteki środowiska wykonawczego. Podczas sprawdzania kodu aplikacji R8 określa, że metody foo(), faz() i bar() są osiągalne z punktu wejścia MainActivity.class. Jednak klasa OkayApi.class ani jej metoda baz() nigdy nie są używane przez aplikację w czasie działania, więc R8 usuwa ten kod podczas zmniejszania aplikacji.

Rysunek 1. Podczas kompilowania R8 tworzy wykres na podstawie połączonych reguł Keep w projekcie, 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 przechowywania określają klasy, których R8 nie należy odrzucać podczas zmniejszania aplikacji, a R8 traktuje je jako możliwe punkty wejścia do aplikacji. Wtyczka Androida do Gradle i AAPT2 automatycznie generują reguły przechowywania, których wymaga większość projektów aplikacji, np. działania, widoki i usługi aplikacji. Jeśli jednak chcesz dostosować to domyślne działanie, dodając dodatkowe reguły Keep, przeczytaj sekcję o tym, jak dostosować kod do zachowania.

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

Pamiętaj, że jeśli projekt biblioteki jest zmniejszony, aplikacja zależna od tej biblioteki zawiera zmniejszone klasy biblioteki. Jeśli w pliku APK biblioteki brakuje klas, być może trzeba będzie dostosować reguły zachowywania biblioteki. Jeśli tworzysz i publikujesz bibliotekę w formacie AAR, lokalne pliki JAR, od których biblioteka jest zależna, nie zostaną zmniejszone w pliku AAR.

Wybierz kod, który chcesz zachować

W większości przypadków domyślny plik reguł ProGuard (proguard-android- optimize.txt) wystarcza do usunięcia nieużywanego kodu przez R8. Jednak w niektórych sytuacjach R8 może utrudnić poprawną analizę i może w efekcie usunąć kod, którego rzeczywiście potrzebuje aplikacja. Oto kilka przykładów sytuacji, w których kod może zostać nieprawidłowo usunięty:

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

Testowanie aplikacji powinno wykazać wszelkie błędy spowodowane niewłaściwie usuniętym kodem, ale możesz też sprawdzić, który kod został usunięty, wygenerując raport dotyczący usuniętego kodu.

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

-keep public class MyClass

Możesz też dodać adnotację @Keep do kodu, który chcesz zachować. Dodanie do klasy obiektu @Keep zachowuje całą klasę bez zmian. Dodanie jej w metodzie lub polu spowoduje, że metoda/pole (oraz jego nazwa) oraz nazwa klasy pozostaną bez zmian. Pamiętaj, że ta adnotacja jest dostępna tylko podczas korzystania z biblioteki adnotacji AndroidX i gdy dołączysz plik reguł ProGuard, który jest w pakiecie z wtyczką Androida Gradle, zgodnie z opisem w sekcji dotyczącej włączania zmniejszania.

Korzystając z opcji -keep, należy wziąć pod uwagę wiele kwestii. Więcej informacji o dostosowywaniu pliku reguł znajdziesz w instrukcji ProGuard. W sekcji Rozwiązywanie problemów znajdziesz inne typowe problemy, które mogą wystąpić podczas usuwania kodu.

Usuń biblioteki natywne

Domyślnie biblioteki kodu natywnego są usuwane z kompilacji wersji aplikacji. Obejmuje to usunięcie tabeli symboli i informacji debugowania zawartych we wszystkich bibliotekach natywnych używanych przez Twoją aplikację. Usunięcie bibliotek kodu natywnego powoduje znaczne zmniejszenie rozmiaru plików, ale nie jest możliwe diagnozowanie awarii w Konsoli Google Play z powodu brakujących informacji (takich jak nazwy klasy i funkcji).

Obsługa natywnych awarii

Konsola Google Play zgłasza awarie natywne w Android Vitals. Wykonując kilka czynności, możesz wygenerować i przesłać plik symboli do debugowania kodu natywnego dla swojej aplikacji. Ten plik udostępnia symbolizowane zrzuty stosu awarii natywnych (obejmujące nazwy klas i funkcji) w Android Vitals, aby ułatwić debugowanie aplikacji w wersji produkcyjnej. Te kroki różnią się w zależności od wersji wtyczki Androida do obsługi Gradle używanej w projekcie oraz danych wyjściowych kompilacji.

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

Jeśli w ramach projektu tworzysz pakiet Android App Bundle, możesz automatycznie uwzględnić w nim plik symboli do debugowania kodu natywnego. Aby uwzględnić ten plik w kompilacjach wersji, do pliku build.gradle.kts aplikacji dodaj ten plik:

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

Wybierz jeden z tych poziomów symboli debugowania:

  • Użyj funkcji SYMBOL_TABLE, aby uzyskać nazwy funkcji w symbolicznych zrzutach stosu w Konsoli Play. Ten poziom obsługuje tombstone.
  • Aby uzyskać nazwy funkcji, pliki i numery wierszy w symbolicznych zrzutach stosu w Konsoli Play, użyj polecenia FULL.

Jeśli Twój projekt tworzy plik APK, użyj ustawienia kompilacji build.gradle.kts opisanego wcześniej, aby wygenerować oddzielnie plik symboli debugowania. Ręcznie prześlij plik symboli debugowania kodu natywnego do Konsoli Google Play. W ramach procesu kompilacji wtyczka Androida do obsługi Gradle generuje ten plik w następującej 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 kopię nieskróconych bibliotek w katalogu projektu. 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 ze zmniejszaniem kodu. Gdy narzędzie do usuwania kodu usunie cały nieużywany kod, ogranicza ono zasoby do identyfikacji zasobów, których nadal używa aplikacja. Jest to szczególnie istotne, gdy dodajesz biblioteki kodu zawierające zasoby. Musisz usunąć nieużywany kod biblioteki, aby zasoby biblioteki przestały się odwoływać, a tym samym można było je usunąć przez zmniejszanie zasobów.

Aby umożliwić zmniejszanie zasobów, ustaw właściwość shrinkResources na true w skrypcie kompilacji (wraz z minifyEnabled, by zmniejszyć kod). Na przykład:

Kotlin

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

Odlotowe

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

Jeśli nie masz jeszcze aplikacji minifyEnabled do zmniejszania kodu, spróbuj to zrobić przed włączeniem funkcji shrinkResources, ponieważ przed usunięciem zasobów konieczne może być zmodyfikowanie pliku proguard-rules.pro w taki sposób, aby zachować klasy lub metody, które są tworzone lub wywoływane dynamicznie.

Wybierz zasoby, które chcesz zachować

Jeśli chcesz zachować lub odrzucić konkretne zasoby, utwórz w projekcie plik XML z tagiem <resources> i wskaż poszczególne zasoby do zachowania w atrybucie tools:keep, a w atrybucie tools:discard poszczególne zasoby, które chcesz odrzucić. Oba atrybuty mogą mieć listę nazw zasobów oddzielonych przecinkami. Możesz jej użyć 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, np. res/raw/keep.xml. Kompilacja nie spakuje tego pliku w aplikacji.

Określanie zasobów do odrzucenia może wydawać się głupie, zamiast je usuwać, ale jest to przydatne w przypadku korzystania z wariantów kompilacji. Możesz na przykład umieścić wszystkie zasoby we wspólnym katalogu projektu, a następnie utworzyć osobny plik keep.xml dla każdego wariantu kompilacji, gdy wiesz, że dany zasób wydaje się być używany w kodzie (i dlatego nie został usunięty przez zmniejszacz), ale wiesz, że w rzeczywistości nie będzie używany w przypadku danego wariantu kompilacji. Możliwe też, że narzędzia do kompilacji nieprawidłowo zidentyfikowały zasób w miarę potrzeb. Jest to możliwe, ponieważ kompilator dodaje identyfikatory zasobów w tekście, więc analizator zasobów może nie rozpoznać różnicy między rzeczywiście przypisanym zasobem a wartością całkowitą w kodzie, która ma taką samą wartość.

Włącz rygorystyczne sprawdzanie odwołań

Zazwyczaj zmniejszające zasoby potrafią dokładnie określić, czy dany zasób jest używany. Jeśli jednak Twój kod wywołuje Resources.getIdentifier() (lub robi to jakaś z Twoich bibliotek – robi to biblioteka AppCompat), oznacza to, że kod wyszukuje nazwy zasobów na podstawie ciągów generowanych dynamicznie. Gdy to zrobisz, zmniejszanie zasobów będzie działać domyślnie 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 oznaczenie wszystkich zasobów z prefiksem img_ jako używanych.

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());

Zmniejsza ono również wszystkie stałe ciągi znaków w kodzie, a także 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 wykryje ciągi znaków takie jak ten lub inne, które mogą być używane do tworzenia adresów URL w ten sposób, nie zostaną usunięte.

Oto przykłady bezpiecznego pomniejszania, który jest domyślnie włączony. Możesz jednak wyłączyć obsługę tego typu zabezpieczeń i określić, że pomniejszający zasoby zachowuje tylko te zasoby, które na pewno zostaną użyte. Aby to zrobić, ustaw w pliku keep.xml wartość shrinkMode na strict w ten 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ę też do zasobów z dynamicznie generowanymi ciągami znaków (jak pokazano powyżej), musisz ręcznie zachowywać te zasoby za pomocą atrybutu tools:keep.

Usuń nieużywane zasoby alternatywne

Zmniejszający zasoby Gradle usuwa tylko zasoby, do których nie odwołuje się kod Twojej aplikacji, co oznacza, ż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ąć pliki zasobów alternatywnych, których aplikacja nie potrzebuje.

Jeśli na przykład używasz biblioteki z zasobami językowymi (takich jak AppCompat czy Usługi Google Play), aplikacja będzie zawierać wszystkie przetłumaczone ciągi językowe wiadomości w tych bibliotekach niezależnie od tego, czy pozostała część aplikacji jest przetłumaczona na te same języki. Jeśli chcesz zachować tylko języki oficjalnie obsługiwane przez Twoją aplikację, możesz je określić za pomocą właściwości resConfig. Wszystkie zasoby nieokreślonych języków zostaną usunięte.

Z tego fragmentu dowiesz się, jak ograniczyć zasoby językowe do języka angielskiego i francuskiego:

Kotlin

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

Odlotowe

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

Gdy publikujesz aplikację w formacie Android App Bundle, domyślnie pobierane są tylko języki skonfigurowane na urządzeniu użytkownika. Podobnie w pobieraniu uwzględniane są tylko zasoby pasujące do gęstości ekranu urządzenia i biblioteki natywne zgodne z interfejsem ABI urządzenia. Więcej informacji znajdziesz w artykule o konfiguracji pakietu aplikacji na Androida.

W przypadku starszych aplikacji publikujących pliki APK (utworzonych przed sierpniem 2021 roku) możesz dostosować gęstość ekranu lub zasoby ABI, które chcesz uwzględnić w pliku APK. W tym celu utwórz wiele plików APK przeznaczonych na inną konfigurację urządzenia.

Scal zduplikowane zasoby

Domyślnie Gradle scala też zasoby o identycznej nazwie, na przykład elementy rysunkowe o tej samej nazwie, które mogą znajdować się w różnych folderach zasobów. Działanie to nie jest kontrolowane przez właściwość shrinkResources i nie można go wyłączyć, ponieważ jest to konieczne, aby uniknąć błędów w przypadku, gdy wiele zasobów pasuje do nazwy wyszukiwanej w kodzie.

Scalanie zasobów ma miejsce tylko wtedy, gdy co najmniej 2 pliki mają taką samą nazwę, typ i kwalifikator zasobu. Gradle wybiera plik, który jest najlepszym wyborem spośród duplikatów (w oparciu o kolejność priorytetu opisaną poniżej) i przekazuje tylko ten jeden zasób do AAPT do dystrybucji w ostatecznym artefaktie.

Gradle szuka zduplikowanych zasobów w tych lokalizacjach:

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

Gradle scala zduplikowane zasoby w tej kaskadowej kolejności według priorytetu:

Zależności → Główny → Rodzaj 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 z rodzaju kompilacji.

Jeśli identyczne zasoby występują w tym samym zbiorze źródłowym, Gradle nie może ich scalić i powoduje błąd scalania zasobów. Może się tak zdarzyć, jeśli w usłudze sourceSet w pliku build.gradle.kts zdefiniujesz wiele zbiorów źródeł – np. gdy src/main/res/ i src/main/res2/ zawierają identyczne zasoby.

Zaciemnianie kodu

Zaciemnianie kodu służy do zmniejszenia rozmiaru aplikacji przez skrócenie nazw jej klas, metod i pól. Oto przykład zaciemniania 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

Zaciemnianie kodu nie powoduje usunięcia kodu z aplikacji, ale w aplikacjach z plikami DEX, które indeksują wiele klas, metod i pól, można zaobserwować spore oszczędności. 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 dowiedzieć się, jak działa zrzut stosu po zaciemnieniu, przeczytaj sekcję o odkodowywaniu zrzutu stosu z zaciemnionym kodem.

Poza tym jeśli kod opiera się na przewidywalnych nazwach metod i klas aplikacji – na przykład przy użyciu odbicia należy traktować te podpisy jako punkty wejścia i określić dla nich reguły zachowywania. Więcej informacji na ten temat znajdziesz w sekcji o dostosowywaniu kodu do zachowania. Dzięki takim regułom R8 ma nie tylko zachować ten kod w ostatecznej wersji DEX aplikacji, ale również zachować oryginalne nazwy.

Dekodowanie zaciemnionego zrzutu stosu

Gdy R8 zaciemnia kod, zrozumienie zrzutu stosu jest trudne (jeśli nie jest to możliwe), ponieważ nazwy klas i metod mogły ulec zmianie. Aby uzyskać pierwotny zrzut stosu, ponownie śledzij zrzut stosu.

Optymalizacja kodu

Aby jeszcze bardziej zoptymalizować aplikację, R8 sprawdza kod na głębszym poziomie i w miarę możliwości usuwa więcej nieużywanego kodu lub w miarę możliwości przerabia go, aby był mniej szczegółowy. Oto kilka przykładów takich optymalizacji:

  • Jeśli Twój kod nigdy nie używa gałęzi else {} w 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 usunąć tę metodę i wstawić ją w tych kilku witrynach wywołujących.
  • Jeśli R8 ustali, że klasa ma tylko 1 unikalną podklasę, a sama klasa nie ma wystąpienia (np. abstrakcyjna klasa bazowa używana tylko przez jedną konkretną klasę implementacji), R8 może połączyć te 2 klasy i usunąć klasę z aplikacji.
  • Więcej informacji znajdziesz w postach na blogu o optymalizacji R8 autorstwa Jake'a Whartona.

R8 nie umożliwia wyłączania ani włączania dyskretnych optymalizacji ani zmiany działania optymalizacji. W rzeczywistości R8 ignoruje wszystkie reguły ProGuard, które próbują modyfikować domyślne optymalizacje, takie jak -optimizations czy -optimizationpasses. To ograniczenie jest ważne, ponieważ w miarę ulepszania R8 zachowanie standardowego działania optymalizacji pomaga zespołowi Android Studio w łatwym rozwiązywaniu problemów, jakie mogą wystąpić.

Pamiętaj, że włączenie optymalizacji spowoduje zmianę zrzutów stosu Twojej aplikacji. Na przykład wbudowanie spowoduje usunięcie ramek stosu. Aby dowiedzieć się, jak uzyskać oryginalne zrzuty stosu, zapoznaj się z sekcją na temat wycofywania.

Wpływ na wydajność środowiska wykonawczego

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

Jeśli włączona jest funkcja R8, warto też utworzyć profile startowe, aby uzyskać jeszcze lepsze wyniki podczas uruchamiania.

Włącz bardziej agresywne optymalizacje

R8 zawiera zestaw dodatkowych optymalizacji (nazywanych „trybem całego ruchu”), dzięki czemu działa inaczej niż ProGuard. Te optymalizacje są domyślnie włączone od wtyczki Androida do obsługi Gradle w wersji 8.0.0.

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

android.enableR8.fullMode=false

Dodatkowe optymalizacje sprawiają, że R8 działa inaczej niż ProGuard, więc może być konieczne dołączenie dodatkowych reguł ProGuard, aby uniknąć problemów w czasie działania, 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ć obiekty tej klasy i manipulować nimi w czasie działania – nawet jeśli Twój kod w rzeczywistości tego nie robi – oraz automatycznie zachowuje klasę i jej statyczny inicjator.

Jednak w „trybie całego ruchu” R8 nie przyjmuje tego założenia i jeśli R8 stwierdzi, że w przeciwnym razie kod nigdy nie używa tej klasy w czasie działania, usuwa tę klasę z ostatecznej wersji DEX aplikacji. Oznacza to, że jeśli chcesz zachować klasę i jej inicjator statyczny, musisz w tym celu umieścić w pliku reguł regułę Keep.

W razie problemów podczas korzystania z „trybu pełnego” w R8 poszukaj możliwego rozwiązania na stronie z najczęstszymi pytaniami dotyczącymi R8. Jeśli nie uda Ci się 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ż zrzuty stosu nie będą dokładnie odpowiadać kodowi źródłowemu. Może to być spowodowane zmianą numerów wierszy, gdy informacje debugowania nie są zachowywane. Może to być spowodowane optymalizowaniem, takim jak wstęp i konspekt. Największym czynnikiem jest zaciemnianie, w którym 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ć cofanie zrzutów stosu aplikacji, upewnij się, że kompilacja zachowuje wystarczającą ilość informacji do ponownego wykonania, dodając te reguły do pliku proguard-rules.pro modułu:

-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

Atrybut LineNumberTable zachowuje informacje o pozycji w metodach, które powodują, że pozycje te są wydrukowane w zrzutach stosu. Atrybut SourceFile gwarantuje, że wszystkie potencjalne środowiska wykonawcze wyświetlają informacje o pozycji. Dyrektywa -renamesourcefileattribute ustawia nazwę pliku źródłowego w śladach stosu tylko na SourceFile. Rzeczywista nazwa oryginalnego pliku źródłowego nie jest wymagana przy przywracaniu, ponieważ plik mapowania zawiera oryginalny plik źródłowy.

Przy każdym uruchomieniu R8 tworzy plik mapping.txt, który zawiera informacje potrzebne do zmapowania zrzutów stosu z powrotem na pierwotne zrzuty stosu. Android Studio zapisze plik w katalogu <module-name>/build/outputs/mapping/<build-type>/.

Podczas publikowania aplikacji w Google Play możesz przesłać plik mapping.txt dla każdej jej wersji. W przypadku publikowania przy użyciu pakietów Android App Bundle ten plik jest automatycznie dołączany do treści pakietu. Następnie Google Play ponownie śledzi przychodzące zrzuty stosu dotyczące problemów zgłoszonych przez użytkowników, aby można je było przejrzeć w Konsoli Play. Więcej informacji znajdziesz w artykule w Centrum pomocy o usuwaniu zaciemnienia kodu w zrzutach stosu awarii.

Rozwiązywanie problemów przy użyciu R8

W tej sekcji opisujemy kilka strategii rozwiązywania problemów z włączaniem ograniczania, zaciemniania i optymalizacji przy użyciu R8. Jeśli poniżej nie znajdziesz rozwiązania swojego problemu, zapoznaj się również ze stroną z najczęstszymi pytaniami na temat R8 i przewodnikiem rozwiązywania problemów ProGuard.

Generowanie raportu o usuniętym (lub zachowanym) kodzie

W celu rozwiązania niektórych problemów z R8 przydatne może być wyświetlenie raportu całego kodu usuniętego z aplikacji przez R8. W przypadku każdego modułu, dla którego chcesz wygenerować ten raport, dodaj -printusage <output-dir>/usage.txt do pliku reguł niestandardowych. Gdy włączysz R8 i skompilujesz aplikację, R8 wygeneruje raport z określoną ś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, uwzględnij -printseeds <output-dir>/seeds.txt w pliku reguł niestandardowych. Gdy włączysz R8 i skompilujesz aplikację, R8 wygeneruje raport o określonej ścieżce i nazwie pliku. Raport z zachowanymi punktami 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 ze zmniejszaniem zasobów

Gdy zmniejszasz zasoby, w oknie Kompilacji wyświetla się podsumowanie zasobów usuniętych z aplikacji. Aby wyświetlić szczegółowe wyniki tekstowe generowane przez Gradle, musisz najpierw kliknąć Przełącz widok po lewej stronie okna. Na przykład:

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

Gradle tworzy również plik diagnostyczny o nazwie resources.txt w <module-name>/build/outputs/mapping/release/ (tym samym folderze co pliki wyjściowe ProGuard). Zawiera on szczegółowe informacje, np. które zasoby odwołują się do innych zasobów oraz które zasoby są używane lub usunięte.

Aby na przykład dowiedzieć się, dlaczego plik @drawable/ic_plus_anim_016 nadal znajduje się w Twojej aplikacji, otwórz plik resources.txt i wyszukaj jego nazwę. Możesz zauważyć, że odwołanie do niego następuje z innego zasobu, w ten sposób:

16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]     @drawable/ic_plus_anim_016

Teraz wiesz już, dlaczego @drawable/add_schedule_fab_icon_anim jest osiągalny. Jeśli wyszukasz wyżej, ten zasób będzie wymieniony w sekcji „Osiągalne zasoby główne to:”. Oznacza to, że występuje odwołanie do kodu add_schedule_fab_icon_anim (czyli jego identyfikator R.drawable został znaleziony w osiągalnym kodzie).

Jeśli nie korzystasz ze ścisłego sprawdzania, identyfikatory zasobów mogą być oznaczone jako osiągalne, o ile występują stałe ciągi znaków, które wyglądają, jakby mogły być używane do tworzenia nazw zasobów na potrzeby zasobów ładowanych dynamicznie. W takim przypadku po wyszukaniu w danych wyjściowych kompilacji nazwy zasobu możesz znaleźć taki 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 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, że ma on go usunąć, jak opisano w sekcji dotyczącej dostosowywania zasobów do zachowania.