Zmniejszanie, zaciemnianie i optymalizowanie aplikacji

Aby Twoja aplikacja była jak najmniejsza i jak najszybsza, zoptymalizuj i zmniejsz jej wersję przeznaczoną do wdrożenia za pomocą narzędzia isMinifyEnabled = true.

Dzięki temu możesz włączyć zmniejszanie kodu, które usuwa nieużywany kod, zaciemnianie kodu, które skraca nazwy klas i elementów aplikacji, oraz optymalizację, która stosuje ulepszone strategie optymalizacji kodu, aby jeszcze bardziej zmniejszyć rozmiar aplikacji i poprawić jej wydajność. Na tej stronie opisaliśmy, jak R8 wykonuje te zadania kompilacji w Twoim projekcie, i jak możesz je dostosowywać.

Gdy kompilujesz projekt za pomocą wtyczki Android Gradle w wersji 3.4.0 lub nowszej, wtyczka nie używa już ProGuarda do optymalizacji kodu w czasie kompilacji. Zamiast tego działa on z kompilatorem R8, aby obsługiwać te zadania wykonywane na etapie kompilacji:

  • Zmniejsz kod (czyli usuń zbędne elementy z drzewa): wykrywa i bezpiecznie usuwa z aplikacji i jej zależności bibliotek nieużywane klasy, pola, metody i atrybuty (co czyni z niego przydatne narzędzie do obejścia limitu 64 tys. odwołań). Jeśli na przykład używasz tylko kilku interfejsów API z biblioteki, która jest zależna od innych bibliotek, zmniejszanie kodu może wykryć kod biblioteki, którego Twoja aplikacja nie używa, i usunąć tylko ten kod. Aby dowiedzieć się więcej, zapoznaj się z sekcją dotyczącą zmniejszania kodu.
  • Skrócenie zasobów: usuwa z zapakowanej aplikacji nieużywane zasoby, w tym te, które nie są używane w bibliotekach zależnych aplikacji. Funkcja ta działa w połączeniu ze zmniejszaniem kodu, dzięki czemu po usunięciu nieużywanego kodu można bezpiecznie usunąć również zasoby, do których nie odwołuje się już kod. Więcej informacji znajdziesz w sekcji poświęconej zmniejszaniu rozmiaru zasobów.
  • Optymalizacja: sprawdza i przepisuje kod, aby zwiększyć wydajność w czasie wykonywania i jeszcze bardziej zmniejszyć rozmiar plików DEX aplikacji. Dzięki temu wydajność kodu w czasie działania zwiększy się nawet o 30%, co znacznie poprawia czas uruchamiania i czas trwania klatek. Jeśli na przykład R8 wykryje, że gałąź else {} w danym instrukcji if/else nigdy nie jest wykonywana, usuwa kod tej gałęzi.else {} Więcej informacji znajdziesz w sekcji optymalizacja kodu.
  • Zaciemnianie (czyli minimalizacja identyfikatorów): skraca nazwy klas i elementów, co powoduje zmniejszenie rozmiaru plików DEX. Więcej informacji znajdziesz w sekcji poświęconej zaciemnianiu kodu.

Podczas kompilowania wersji aplikacji przeznaczonej do publikacji możesz skonfigurować narzędzie R8 tak, aby wykonywało opisane powyżej zadania w czasie 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 dotychczasowymi plikami reguł ProGuard, więc aktualizacja wtyczki Android Gradle do obsługi R8 nie powinna wymagać zmiany dotychczasowych reguł.

Włącz kompresowanie, zaciemnianie i optymalizację

Jeśli używasz Android Studio 3.4 lub wtyczki Gradle dla Androida w wersji 3.4.0 lub nowszej, R8 jest domyślnym kompilatorem, który konwertuje bajtkod Java projektu na format DEX, który działa na platformie Androida. Jednak podczas tworzenia nowego projektu za pomocą Android Studio kompresja, zaciemnianie i optymalizacja kodu nie są domyślnie włączone. Dzieje się tak, ponieważ optymalizacje na etapie kompilacji wydłużają czas kompilacji projektu i mogą powodować błędy, jeśli nie wystarczająco spersonalizujesz kod, który ma zostać zachowany.

Dlatego najlepiej włączyć te zadania kompilacji podczas tworzenia ostatecznej wersji aplikacji, którą testujesz przed opublikowaniem. Aby włączyć kompresowanie, zaciemnianie i optymalizację, umieść w skrypcie kompilacji na poziomie projektu następujące instrukcje.

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

Groovy

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, aby modyfikować swoje domyślne zachowanie i lepiej rozumieć strukturę aplikacji, np. klasy, które służą jako punkty wejścia do kodu aplikacji. Chociaż możesz modyfikować niektóre z tych plików z regułami, niektóre z nich mogą być generowane automatycznie przez narzędzia kompilacji, takie jak AAPT2, lub dziedziczone z zależnych bibliotek aplikacji. Tabela poniżej zawiera informacje o źródłach plików reguł ProGuard, których używa R8.

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

Domyślnie ten plik nie stosuje żadnych reguł. Uwzględnij tutaj własne reguły ProGuard, takie jak niestandardowe reguły przechowywania.

Wtyczka Androida do obsługi Gradle Wygenerowany przez wtyczkę Androida do obsługi Gradle w momencie 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 umożliwia dodawanie adnotacji @Keep*.

Podczas tworzenia nowego modułu za pomocą Android Studio skrypt kompilacji na poziomie modułu automatycznie uwzględnia ten plik reguł w kompilacji wersji.

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

Zależności bibliotek

W bibliotece AAR:
proguard.txt

W bibliotece JAR:
META-INF/proguard/<ProGuard-rules-file>

Oprócz tych lokalizacji wtyczka Gradle na Androida w wersji 3.6 lub nowszej obsługuje też reguły kompresji kierunkowej.

Jeśli biblioteka AAR lub JAR jest opublikowana z własnym plikiem reguł, a Ty uwzględnisz tę bibliotekę jako zależność kompilacji, R8 automatycznie zastosuje te reguły podczas kompilowania projektu.

Oprócz konwencjonalnych reguł ProGuard wtyczka Androida do obsługi Gradle w wersji 3.6 lub nowszej obsługuje również reguły ukierunkowanego kompresowania. Są to reguły kierowane na konkretne narzędzia do kompresji (R8 lub ProGuard) oraz ich konkretne wersje.

Korzystanie z plików reguł dołączonych do bibliotek jest przydatne, jeśli do prawidłowego działania biblioteki wymagane są określone reguły. Oznacza to, że deweloper biblioteki wykonał za Ciebie czynności rozwiązywania problemów.

Pamiętaj jednak, że ponieważ reguły są sumowane, niektórych reguł zawartych w bibliotece zależnej nie można usunąć, ponieważ mogą one wpływać na kompilację innych części aplikacji. Jeśli na przykład biblioteka zawiera regułę wyłączającą optymalizację kodu, reguła ta wyłącza optymalizację całego projektu.

Android Asset Package Tool 2 (AAPT2) Po skompilowaniu projektu za pomocą narzędzia minifyEnabled true:<module-dir>/build/intermediates/aapt_proguard_file/.../aapt_rules.txt AAPT2 generuje reguły przechowywania na podstawie odwołań do klas w pliku manifestu, układach i innych zasobach aplikacji. AAPT2 zawiera na przykład regułę keep dla każdej aktywności zarejestrowanej w pliku manifestu aplikacji jako punkt wejścia.
Pliki konfiguracji niestandardowych Podczas tworzenia nowego modułu za pomocą Android Studio IDE domyślnie tworzy <module-dir>/proguard-rules.pro, aby umożliwić dodawanie własnych reguł. Możesz uwzględnić dodatkowe konfiguracje, które R8 zastosuje w czasie kompilacji.

Gdy ustawisz wartość właściwości minifyEnabled na true, R8 połączy reguły ze wszystkich wymienionych powyżej dostępnych źródeł. Pamiętaj o tym podczas rozwiązywania problemów z R8, ponieważ inne zależności na etapie kompilacji, takie jak zależności bibliotek, mogą wprowadzać zmiany w zachowaniu R8, o których nie wiesz.

Aby wygenerować pełny raport ze wszystkimi regułami stosowanymi przez R8 podczas kompilowania projektu, w pliku proguard-rules.pro modułu umieść następujące informacje:

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

Reguły dopasowywania do rozmiaru

Wtyczka Gradle na Androida w wersji 3.6 lub nowszej obsługuje reguły bibliotek, które kierują na konkretne narzędzia do kompresji (R8 lub ProGuard), a także na konkretne wersje tych narzędzi. Dzięki temu deweloperzy bibliotek mogą dostosować swoje reguły, aby działały optymalnie w projektach korzystających z nowych wersji shrinkera, a zarazem umożliwić korzystanie z dotychczasowych reguł w projektach z starszymi wersjami shrinkera.

Aby określić reguły kompresji docelowej, deweloperzy bibliotek muszą uwzględnić je w określonych miejscach w bibliotece AAR lub JAR, jak opisano poniżej.

In an AAR library:
    proguard.txt (legacy location)
    classes.jar
    └── META-INF
        └── com.android.tools (targeted shrink rules location)
            ├── r8-from-<X>-upto-<Y>/<R8-rules-file>
            └── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>

In a JAR library:
    META-INF
    ├── proguard/<ProGuard-rules-file> (legacy location)
    └── com.android.tools (targeted shrink rules location)
        ├── r8-from-<X>-upto-<Y>/<R8-rules-file>
        └── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>

Oznacza to, że reguły ukierunkowanego kompresowania są przechowywane w katalogu META-INF/com.android.tools pliku JAR lub w katalogu META-INF/com.android.tools w pliku classes.jar pliku AAR.

W tym katalogu może się znajdować wiele katalogów o nazwach w formacie r8-from-<X>-upto-<Y> lub proguard-from-<X>-upto-<Y>, które wskazują, dla których wersji shrinkera zostały napisane reguły w katalogach. Pamiętaj, że części -from-<X>-upto-<Y> są opcjonalne, wersja <Y> jest wyłączna, a zakresy wersji muszą być ciągłe.

Na przykład r8-upto-8.0.0, r8-from-8.0.0-upto-8.2.0r8-from-8.2.0 tworzą prawidłowy zestaw reguł skurczenia docelowego. Reguły w katalogu r8-from-8.0.0-upto-8.2.0 będą używane przez R8 od wersji 8.0.0 do z wyjątkiem wersji 8.2.0.

Na podstawie tych informacji wtyczka Gradle dla Androida w wersji 3.6 lub nowszej wybierze reguły z odpowiednich katalogów R8. Jeśli biblioteka nie określa reguł kompresji, wtyczka Gradle dla Androida wybierze reguły z starszych lokalizacji (proguard.txt w przypadku pliku AAR lub META-INF/proguard/<ProGuard-rules-file> w przypadku pliku JAR).

Deweloperzy bibliotek mogą uwzględnić w swoich bibliotekach albo reguły docelowego kompresowania, albo starsze reguły ProGuard, albo oba te typy, jeśli chcą zachować zgodność z wtyczką Android Gradle starszą niż 3.6 lub innymi narzędziami.

Uwzględnij dodatkowe konfiguracje

Gdy tworzysz nowy projekt lub moduł za pomocą Android Studio, IDE utworzy 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 do właściwości proguardFiles w skrypcie kompilacji modułu.

Możesz na przykład dodać reguły, które będą dotyczyć tylko konkretnej wersji kompilacji, dodając inną właściwość proguardFiles w odpowiednim bloku productFlavor. Poniższy plik Gradle dodaje flavor2-rules.pro do wersji produktu flavor2. Teraz flavor2 używa wszystkich 3 reguł ProGuard, ponieważ reguły z bloku release są też stosowane.

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

Groovy

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'
        }
    }
}

Kompilowanie kodu

Kompilacja kodu z R8 jest domyślnie włączona, gdy ustawisz wartość właściwości minifyEnabled na true.

Zmniejszenie kodu (znane też jako usuwanie zbędących elementów drzewa) to proces usuwania kodu, który według R8 nie jest wymagany w czasie wykonywania. Ten proces może znacznie zmniejszyć rozmiar aplikacji, jeśli na przykład zawiera ona wiele zależności od bibliotek, ale wykorzystuje tylko niewielką część ich funkcji.

Aby zoptymalizować kod aplikacji, R8 najpierw określa wszystkie punkty wejścia do kodu aplikacji na podstawie połączonego zbioru plików konfiguracyjnych. Te punkty wejścia obejmują wszystkie klasy, których platforma Android może używać do otwierania czynności lub usług w aplikacji. Zaczynają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 uzyskać dostęp w czasie wykonywania. Kod, który nie jest połączony z tym grafem, jest uważany za nieosiągalny i może zostać usunięty z aplikacji.

Rysunek 1 przedstawia aplikację z zależnością od biblioteki w czasie wykonywania. Podczas sprawdzania kodu aplikacji R8 stwierdza, że metody foo(), faz()bar() są dostępne z punktu wejścia MainActivity.class. Jednak klasa OkayApi.class ani jej metoda baz() nigdy nie są używane przez aplikację w czasie wykonywania, a R8 usuwa ten kod podczas kompresji aplikacji.

Rysunek 1. W czasie kompilacji R8 tworzy graf na podstawie połączonych reguł zachowania Twojego projektu, aby określić niedostępny kod.

R8 określa punkty wejścia za pomocą reguł -keep w plikach konfiguracji R8 projektu. Oznacza to, że reguły zachowania określają klasy, których R8 nie powinien odrzucać podczas kompresowania aplikacji, a R8 uważa te klasy za możliwe punkty wejścia do aplikacji. Wtyczka Gradle dla Androida i AAPT2 automatycznie generują reguły zachowania wymagane przez większość projektów aplikacji, takie jak czynności, widoki i usługi aplikacji. Jeśli jednak chcesz dostosować to domyślne działanie za pomocą dodatkowych reguł przechowywania, przeczytaj sekcję o dostosowywaniu kodu, który ma być przechowywany.

Jeśli interesuje Cię tylko zmniejszenie rozmiaru zasobów aplikacji, przejdź do sekcji Zmniejszanie rozmiaru zasobów.

Pamiętaj, że jeśli projekt biblioteki jest zredukowany, aplikacja, która jest od niego zależna, zawiera zredukowane klasy biblioteki. Jeśli w pliku APK biblioteki brakuje klas, konieczne może być dostosowanie reguł przechowywania biblioteki. Jeśli kompilujesz i publikujesz bibliotekę w formacie AAR, lokalne pliki JAR, od których zależy biblioteka, nie są kompresowane w pliku AAR.

Dostosowywanie kodu do zachowania

W większości przypadków domyślny plik reguł ProGuard (proguard-android-optimize.txt) wystarczy, aby R8 usunęło tylko nieużywany kod. Jednak w niektórych sytuacjach R8 może mieć trudności z prawidłowym przeanalizowaniem kodu i może usunąć kod, którego aplikacja rzeczywiście potrzebuje. Oto kilka przykładów sytuacji, w których może ono nieprawidłowo usuwać kod:

  • gdy aplikacja wywołuje metodę z interfejsu JNI (Java Native Interface);
  • gdy aplikacja wyszukuje kod w czasie wykonywania (np. w ramach działania funkcji odbicia lustrzanego);

Testowanie aplikacji powinno ujawnić wszelkie błędy spowodowane nieprawidłowo usuniętym kodem. Możesz też sprawdzić, jaki kod został usunięty, generując raport o usuniętym kodzie.

Aby naprawić błędy i zmusić R8 do zachowania określonego kodu, dodaj wiersz-keepw pliku reguł ProGuard. Przykład:

-keep public class MyClass

Możesz też dodać adnotację @Keep do kodu, który chcesz zachować. Dodanie @Keep do klasy powoduje zachowanie całej klasy w jej pierwotnej postaci. Dodanie @Keep do metody lub pola powoduje zachowanie metody/pola (oraz jej nazwy), a także nazwy klasy. Pamiętaj, że ta adnotacja jest dostępna tylko wtedy, gdy używasz biblioteki adnotacji AndroidX i uwzględniasz plik reguł ProGuard, który jest zapakowany z wtyczką Gradle dla Androida, zgodnie z opisem w sekcji Włączanie kompresji.

Podczas korzystania z opcji -keep należy wziąć pod uwagę wiele kwestii. Więcej informacji o dostosowywaniu pliku reguł znajdziesz w podręczniku ProGuard. W sekcji Rozwiązywanie problemów znajdziesz informacje o innych typowych problemach, które mogą wystąpić, gdy kod zostanie usunięty.

Usuwanie bibliotek natywnych

Domyślnie w wersjach publikowanych aplikacji usuwane są natywne biblioteki kodu. Polega to na usunięciu tabeli symboli i informacji debugujących zawartych w dowolnych natywnych bibliotekach używanych przez aplikację. Usuwanie natywnych bibliotek kodu powoduje znaczne zmniejszenie rozmiaru, ale uniemożliwia diagnozowanie awarii w Konsoli Google Play z powodu braku informacji (np. nazw klas i funkcji).

Pomoc w przypadku awarii systemowej

Konsola Google Play zgłasza awarie natywnych aplikacji w ramach Android Vitals. Wystarczy kilka kroków, aby wygenerować i przesłać plik z symbolami do debugowania kodu natywnego dla aplikacji. Ten plik umożliwia w Android Vitals symbolizowanie ścieżek wywołań kodu natywnego (w tym nazwy klas i funkcji), co ułatwia debugowanie aplikacji w produkcji. Te kroki różnią się w zależności od wersji wtyczki Androida do obsługi Gradle używanej w projekcie i wyniku kompilacji projektu.

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 z symbolami do debugowania kodu natywnego. Aby uwzględnić ten plik w kompilacji wersji, dodaj do pliku build.gradle.kts aplikacji te informacje:

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

Wybierz poziom symboli debugowania spośród tych opcji:

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

Jeśli w ramach projektu tworzysz plik APK, użyj ustawienia kompilacji build.gradle.kts, aby niezależnie wygenerować plik z symbolami. 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 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 nowszej (i inne systemy kompilacji)

W ramach procesu kompilacji wtyczka Androida do obsługi Gradle przechowuje kopię niezmodyfikowanych 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.

Zmniejsz zasoby

Zmniejszenie zasobów działa tylko w połączeniu ze zmniejszeniem kodu. Gdy narzędzie do zmniejszania kodu usunie cały nieużywany kod, narzędzie do zmniejszania zasobów może określić, których zasobów aplikacja nadal używa. Jest to szczególnie ważne w przypadku dodawania bibliotek kodu zawierających zasoby. Musisz usunąć nieużywany kod biblioteki, aby zasoby biblioteki nie były już odwoływane, a tym samym mogły zostać usunięte przez narzędzie do zmniejszania rozmiaru zasobów.

Aby włączyć kompresję zasobów, ustaw w swoim skrypcie kompilacji właściwość shrinkResources na true (oraz minifyEnabled w przypadku kompresji kodu). Przykład:

Kotlin

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

Groovy

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

Jeśli nie masz jeszcze aplikacji skompilowanej za pomocą minifyEnabled w celu zmniejszenia rozmiaru kodu, spróbuj to zrobić przed włączeniem shrinkResources, ponieważ przed rozpoczęciem usuwania zasobów może być konieczne zmodyfikowanie pliku proguard-rules.pro, aby zachować klasy lub metody tworzone lub wywoływane dynamicznie.

Dostosowywanie zasobów do zachowania

Jeśli chcesz zachować lub odrzucić konkretne zasoby, utwórz w projekcie plik XML z tagiem <resources> i wskaż w atrybucie tools:keep zasoby, które mają zostać zachowane, oraz w atrybucie tools:discard zasoby, które mają zostać odrzucone. Oba atrybuty akceptują listę nazw zasobów rozdzieloną przecinkami. Możesz użyć gwiazdki jako symbolu wieloznacznego.

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. w folderze res/raw/my.package.keep.xml. Kompilacja nie pakuje tego pliku do aplikacji.

Uwaga: plik keep musi mieć unikalną nazwę. Gdy różne biblioteki są połączone, ich reguły zachowania będą ze sobą kolidować, co może spowodować problemy z ignorowanymi regułami lub niepotrzebnie zachowanymi zasobami.

Określanie zasobów do odrzucenia może wydawać się dziwne, skoro można je po prostu usunąć, ale może być przydatne podczas korzystania z wersji kompilacji. Możesz np. umieścić wszystkie zasoby w wspólnym katalogu projektu, a potem utworzyć dla każdego wariantu kompilacji inny plik my.package.build.variant.keep.xml, jeśli wiesz, że dany zasób jest używany w kodzie (i dlatego nie został usunięty przez kompresję), ale wiesz też, że nie będzie używany w danym wariancie kompilacji. Możliwe też, że narzędzia kompilacji nieprawidłowo zidentyfikowały zasób jako potrzebny. Dzieje się tak, ponieważ kompilator dodaje identyfikatory zasobów w kodziku, a analizator zasobów może nie znać różnicy między rzeczywiście odwołującym się do zasobu zasobem a wartością całkowitą w kodzie, która ma taką samą wartość.

Włączanie ścisłych kontroli odwołań

Zazwyczaj narzędzie do kompresji zasobów może dokładnie określić, czy dany zasób jest używany. Jeśli jednak Twój kod wywołuje funkcję Resources.getIdentifier() (lub jeśli robi to któraś z Twoich bibliotek – na przykład biblioteka AppCompat), oznacza to, że Twój kod wyszukuje nazwy zasobów na podstawie dynamicznie generowanych ciągów znaków. W takim przypadku kompresor zasobów domyślnie działa defensywnie i oznacza wszystkie zasoby o odpowiednim formacie nazwy jako potencjalnie używane i niedostępne do usunięcia.

Na przykład podany niżej kod powoduje, że wszystkie zasoby z preiksem img_ są oznaczane jako wykorzystane.

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

Narzędzie do kompresji zasobów sprawdza też wszystkie ciągi znaków w kodzie oraz różne zasoby res/raw/ pod kątem adresów URL zasobów w formacie podobnym do file:///android_res/drawable//ic_plus_anim_016.png. Jeśli znajdzie ciągi takie jak ten lub inne, które wyglądają na takie, że można ich użyć do tworzenia takich adresów URL, nie usuwa ich.

Oto przykłady bezpiecznego trybu zmniejszania, który jest domyślnie włączony. Możesz jednak wyłączyć tę opcję „lepiej dmuchać na zimno” i określić, aby narzędzie do kompresji zasobów zachowywało tylko te zasoby, których używasz. Aby to zrobić, ustaw wartość shrinkMode na strict w pliku keep.xml 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 ścisły tryb kompresji, a Twój kod będzie się też odwoływać do zasobów za pomocą dynamicznie generowanych ciągów znaków, jak pokazano powyżej, musisz ręcznie zachować te zasoby za pomocą atrybutu tools:keep.

Usuwanie nieużywanych zasobów alternatywnych

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

Jeśli na przykład używasz biblioteki zawierającej zasoby językowe (np. AppCompat lub Usługi Google Play), Twoja aplikacja zawiera wszystkie przetłumaczone ciągi znaków dla 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 języki, które są oficjalnie obsługiwane przez aplikację, możesz określić te języki za pomocą właściwości resConfig. Wszystkie zasoby dla języków, których nie wybrano, 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"))
    }
}

Groovy

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

Podczas publikowania aplikacji w formacie pakietu aplikacji na Androida domyślnie podczas instalacji pobierane są tylko języki skonfigurowane na urządzeniu użytkownika. Podobnie tylko zasoby pasujące do gęstości ekranu urządzenia i biblioteki natywne pasujące do ABI urządzenia są uwzględniane w pobieraniu. Więcej informacji znajdziesz w konfiguracji pakietów aplikacji na Androida.

W przypadku starszych aplikacji publikowanych jako pliki APK (utworzonych przed sierpniem 2021 roku) możesz określić, które zasoby gęstości ekranu lub interfejsu ABI uwzględnić w pliku APK. Aby to zrobić, utwórz wiele plików APK, z których każdy będzie kierowany do innej konfiguracji urządzenia.

Scalanie zduplikowanych zasobów

Domyślnie Gradle łączy też zasoby o identycznych nazwach, np. zasoby drawable o tej samej nazwie, które mogą znajdować się w różnych folderach zasobów. To zachowanie nie jest kontrolowane przez właściwość shrinkResources i nie można jej wyłączyć, ponieważ jest ona niezbędna, aby uniknąć błędów, gdy wiele zasobów pasuje do nazwy wyszukiwanej przez kod.

Scalanie zasobów ma miejsce tylko wtedy, gdy co najmniej 2 pliki mają identyczną nazwę, typ i kwalifikator zasobu. Gradle wybiera plik, który według niego jest najlepszym wyborem spośród duplikatów (na podstawie kolejności priorytetów opisanej poniżej) i przekazuje tylko ten zasób do AAPT na potrzeby dystrybucji w pliku końcowym.

Gradle szuka zduplikowanych zasobów w tych lokalizacjach:

  • Główne zasoby powiązane z głównym zbiorem źródeł, które zwykle znajdują się w folderze src/main/res/.
  • Warianty nakładek, z uwzględnieniem typu i wersji kompilacji.
  • Zależności projektu biblioteki.

Gradle scala zduplikowane zasoby w takim porządku priorytetów:

Zależność → Główna → Wersja kompilacji → Typ kompilacji

Jeśli na przykład w głównych zasobach i wersji smaku występuje duplikat zasobu, Gradle wybierze ten z wersji smaku.

Jeśli w tym samym zbiorze źródeł występują identyczne zasoby, Gradle nie może ich scalić i wyświetla błąd scalania zasobów. Może się tak zdarzyć, jeśli zdefiniujesz wiele zbiorów źródeł we właściwości sourceSet pliku build.gradle.kts. Może się tak zdarzyć, jeśli zarówno src/main/res/, jak i src/main/res2/ zawierają te same zasoby.

Ukrywanie kodu

Zaciemnianie kodu ma na celu zmniejszenie rozmiaru aplikacji przez skrócenie nazw klas, metod i pól. Oto przykład zaciemnienia 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 usuwa kodu z aplikacji, ale może znacznie zmniejszyć rozmiar aplikacji z plikami DEX, które indeksują wiele klas, metod i pól. Jednak ponieważ zaciemnianie zmienia nazwy różnych części kodu, niektóre zadania, takie jak sprawdzanie ścieżek stosu, wymagają dodatkowych narzędzi. Aby zrozumieć zrzut stosu po zaciemnieniu, przeczytaj sekcję Dekodowanie zaciemnionego zrzutu stosu.

Jeśli kod opiera się na przewidywalnym nazewnictwie metod i klas aplikacji (np. gdy używasz odbicia lustrzanego), powinieneś traktować te sygnatury jako punkty wejścia i określać dla nich reguły zachowania, jak opisano w sekcji dostosowywanie reguł zachowania kodu. Te reguły informują R8, aby nie tylko zachować ten kod w pliku DEX końcowego pakietu aplikacji, ale też zachować jego pierwotną nazwę.

Dekodowanie zaciemnionego zrzutu stosu

Po zaciemnieniu kodu przez R8 analiza ścieżki wywołania jest trudna (a czasem niemożliwa), ponieważ nazwy klas i metod mogą ulec zmianie. Aby uzyskać pierwotny zrzut stosu, ponownie prześledź zrzut stosu.

Optymalizacja kodu

Aby jeszcze bardziej zoptymalizować aplikację, R8 sprawdza kod na głębszym poziomie, aby usunąć nieużywany kod lub, jeśli to możliwe, przepisać kod, aby był mniej zwięzły. Oto kilka przykładów takich optymalizacji:

  • Jeśli Twój kod nigdy nie wybiera gałęzi else {} w danym instrukcji if/else, R8 może usunąć kod z 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 miejscach wywołania.
  • Jeśli R8 stwierdzi, że klasa ma tylko jedną unikalną podklasę, a sama klasa nie jest instancjonowana (np. abstrakcyjna klasa bazowa używana tylko przez jedną konkretną klasę implementacji), R8 może połączyć te 2 klasy i usunąć jedną z nich z aplikacji.
  • Aby dowiedzieć się więcej, przeczytaj posty na blogu Jake’a Whartona dotyczące optymalizacji R8.

R8 nie pozwala wyłączać ani włączać poszczególnych optymalizacji ani modyfikować ich działania. R8 ignoruje wszystkie reguły ProGuarda, które próbują zmodyfikować domyślne optymalizacje, np. -optimizations-optimizationpasses. To ograniczenie jest ważne, ponieważ R8 jest stale ulepszany, a utrzymanie standardowego zachowania optymalizacji ułatwia zespołowi Android Studio łatwe rozwiązywanie problemów, które mogą się pojawić.

Pamiętaj, że włączenie optymalizacji spowoduje zmianę ścieżek pakietu Twojej aplikacji. Na przykład wstawianie kodu usunie ramki stosu. Aby dowiedzieć się, jak uzyskać oryginalne ścieżki z wykresów, zapoznaj się z sekcją śledzenie.

Wpływ na wydajność w czasie wykonywania

Jeśli kompresja, zaciemnianie i optymalizacja są włączone, R8 poprawi wydajność kodu w czasie wykonywania (w tym czas uruchamiania i czas generowania klatek w wątku interfejsu użytkownika) nawet o 30%. Wyłączenie dowolnej z nich znacznie ogranicza zestaw optymalizacji używanych przez R8.

Jeśli R8 jest włączone, musisz też utworzyć profile uruchamiania, aby jeszcze bardziej poprawić wydajność uruchamiania.

Włączanie ulepszonych optymalizacji

R8 zawiera zestaw dodatkowych optymalizacji (zwanych „trybem pełnym”), dzięki którym działa on inaczej niż ProGuard. Te optymalizacje są domyślnie włączone od wersji 8.0.0 wtyczki Androida do obsługi Gradle.

Możesz wyłączyć te dodatkowe optymalizacje, dodając do pliku gradle.properties projektu te informacje:

android.enableR8.fullMode=false

Dodatkowe optymalizacje powodują, że R8 działa inaczej niż ProGuard, dlatego jeśli używasz reguł zaprojektowanych pod kątem ProGuard, konieczne może być uwzględnienie dodatkowych reguł ProGuard, aby uniknąć problemów w czasie wykonywania. Załóżmy na przykład, że Twój kod odwołuje się do klasy za pomocą interfejsu Java Reflection API. Jeśli 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 tego nie robi), i automatycznie zachowuje klasę oraz jej statyczny inicjalizator.

Jednak w „trybie pełnym” R8 nie zakłada tego i jeśli stwierdzi, że Twój kod nigdy nie używa tej klasy w czasie wykonywania, usuwa ją z ostatecznego pliku DEX aplikacji. Oznacza to, że jeśli chcesz zachować klasę i jej statyczny inicjalizator, musisz w pliku z regułami umieścić regułę zachowania.

Jeśli napotkasz problemy podczas korzystania z „trybu pełnego” R8, zapoznaj się z najczęstszymi pytaniami dotyczącymi R8, aby znaleźć możliwe rozwiązanie. Jeśli nie możesz rozwiązać problemu, zgłoś błąd.

Prześledzenie zrzutów stosu

Kod przetwarzany przez R8 jest zmieniany na różne sposoby, co może utrudniać interpretację dzienników stosu, ponieważ nie będą one dokładnie odpowiadać kodom źródłowym. Może się tak zdarzyć w przypadku zmian numerów wierszy, gdy informacje debugowania nie są przechowywane. Może to być spowodowane optymalizacją, np. wstawianiem i wyznaczaniem obrysów. Największy wpływ ma zaciemnianie, w którym nawet klasy i metody zmieniają nazwy.

Aby odzyskać oryginalny ślad stosu, R8 udostępnia narzędzie wiersza poleceń retrace, które jest dołączone do pakietu narzędzi wiersza poleceń.

Aby umożliwić odtworzenie ścieżek stosu aplikacji, musisz zadbać o to, aby kompilacja zawierała wystarczającą ilość informacji do odtworzenia. Aby to zrobić, dodaj do pliku proguard-rules.pro modułu te reguły:

-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

Atrybut LineNumberTable zachowuje informacje o pozycji w metodach, dzięki czemu pozycje te są drukowane w śladach stosu. Atrybut SourceFile zapewnia, że wszystkie potencjalne czasy wykonywania faktycznie wydrukują informacje o pozycji. Dyrektywa -renamesourcefileattribute ustawia nazwę pliku źródłowego w wyświetleniu ścieżki wywołań na SourceFile. Podczas odtwarzania nie jest wymagana rzeczywista nazwa oryginalnego pliku źródłowego, ponieważ plik mapowania zawiera oryginalny plik źródłowy.

Za każdym razem, gdy narzędzie R8 jest uruchamiane, tworzy ono plik mapping.txt, który zawiera informacje potrzebne do zmapowania ścieżek stosu z pierwotnymi ścieżkami stosu. Android Studio zapisuje plik w folderze <module-name>/build/outputs/mapping/<build-type>/.

Podczas publikowania aplikacji w Google Play możesz przesłać plik mapping.txt dla każdej wersji aplikacji. Podczas publikowania pakietu aplikacji na Androida plik ten jest automatycznie dołączany do treści pakietu. Następnie Google Play będzie śledzić przychodzące ścieżki sterowania z problemów zgłoszonych przez użytkowników, aby można było je sprawdzić w Konsoli Play. Więcej informacji znajdziesz w artykule w Centrum pomocy poświęconym deobfuscacji ścieżek wywołań błędów.

Rozwiązywanie problemów z R8

W tej sekcji opisujemy kilka strategii rozwiązywania problemów występujących po włączeniu skracania, zaciemniania i optymalizacji za pomocą R8. Jeśli nie znajdziesz rozwiązania swojego problemu poniżej, przeczytaj stronę z informacjami o R8przewodnik po ProGuard na temat rozwiązywania problemów.

Generowanie raportu o usuniętym (lub zachowanym) kodzie

Aby ułatwić sobie rozwiązywanie niektórych problemów z R8, możesz wyświetlić raport dotyczący całego kodu, który R8 usunął z aplikacji. W przypadku każdego modułu, dla którego chcesz wygenerować ten raport, dodaj do pliku z niestandardowymi regułami znak -printusage <output-dir>/usage.txt. Gdy włączysz R8 i skompilujesz aplikację, R8 wygeneruje raport z określoną przez Ciebie ścieżką i nazwą pliku. Raport o usuniętym kodzie wygląda mniej więcej tak:

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 chcesz zamiast tego wyświetlić raport z punktami wejścia określonymi przez R8 na podstawie reguł zachowania Twojego projektu , dodaj do pliku niestandardowych reguł element -printseeds <output-dir>/seeds.txt. Gdy włączysz R8 i skompilujesz aplikację, R8 wygeneruje raport z określonymi przez Ciebie ścieżką i nazwą pliku. Raport z zachowanymi punktami wejścia wygląda mniej więcej tak:

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 zmniejszysz zasoby, w oknie Kompiluj wyświetli się podsumowanie zasobów, które zostały usunięte z aplikacji. (Aby wyświetlić szczegółowy tekst z Gradle, musisz najpierw kliknąć Przełącz widok po lewej stronie okna). Przykład:

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

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

Jeśli na przykład chcesz się dowiedzieć, dlaczego @drawable/ic_plus_anim_016 nadal znajduje się w aplikacji, otwórz plik resources.txt i poszukaj tej nazwy. Możesz zauważyć, że jest ono używane przez inny zasób 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

Musisz teraz wiedzieć, dlaczego @drawable/add_schedule_fab_icon_anim jest dostępny. Jeśli przeszukasz ścieżkę do tego zasobu, zobaczysz, że jest on wymieniony w sekcji „Zasoby dostępne na poziomie katalogu ROOT:”. Oznacza to, że w kodzie jest odwołanie do add_schedule_fab_icon_anim (czyli w kodzie dostępnym w ramach aplikacji znaleziono identyfikator R.drawable).

Jeśli nie używasz ścisłej weryfikacji, identyfikatory zasobów mogą zostać oznaczone jako dostępne, jeśli istnieją ciągi znaków stałych, które wyglądają na takie, które mogą być używane do tworzenia nazw zasobów wczytywanych dynamicznie. W takim przypadku, jeśli w wyniku kompilacji wyszukasz nazwę zasobu, możesz zobaczyć 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 widzisz jeden z tych ciągów tekstowych i masz pewność, że nie jest on używany do dynamicznego wczytywania danego zasobu, możesz użyć atrybutu tools:discard, aby poinformować system kompilacji o jego usunięciu. Więcej informacji znajdziesz w sekcji dostosowywanie zasobów do zachowania.