Aby 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 włączysz 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 w celu dalszego zmniejszania rozmiaru aplikacji i poprawienia jej wydajności. Na tej stronie znajdziesz informacje o tym, jak R8 wykonuje te zadania w czasie kompilacji w Twoim projekcie, i jak możesz je dostosowywać.
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 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 go przydatnym narzędziem 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ąć wszystkie 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 wyświetlania poszczególnych klatek. Jeśli na przykład R8 wykryje, że gałąź
else {}
w danym instrukcji if/else nigdy nie jest wykonywana, usuwa kod z tej gałęzi.else {}
Więcej informacji znajdziesz w sekcji poświęconej optymalizacji 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 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 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 w 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ę, dodaj w skrypcie kompilacji na poziomie projektu następujące polecenia.
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, 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 źródła 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 podczas kompilowania. | Wtyczka Androida do obsługi Gradle generuje plik proguard-android-optimize.txt , który zawiera reguły przydatne w przypadku większości projektów na Androida i umożliwia dodawanie 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 |
Zależności bibliotek |
W bibliotece AAR:
W bibliotece JAR: Oprócz tych lokalizacji wtyczka Gradle na Androida w wersji 3.6 lub nowszej obsługuje też reguły kompresji kierowanej. |
Jeśli biblioteka AAR lub JAR została opublikowana z własnym plikiem reguł i dodasz ją jako zależność podczas kompilacji, R8 automatycznie stosuje 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 kompresory (R8 lub ProGuard) oraz na konkretne wersje kompresorów. 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. 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. |
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, 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
Docelowe reguły ograniczania
Wtyczka Gradle dla 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ć docelowe reguły zmniejszenia, programiści bibliotek muszą umieścić je w określonych lokalizacjach w bibliotece AAR lub JAR w sposób opisany 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>
i -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.0
i r8-from-8.2.0
tworzą prawidłowy zestaw docelowych reguł ograniczania. Reguły w katalogu r8-from-8.0.0-upto-8.2.0
będą używane w wersji 8.0.0 od wersji 8.0.0 do 8.2.0 bez wersji 8.2.0.
Biorąc pod uwagę te informacje, wtyczka Androida do obsługi Gradle w wersji 3.6 lub nowszej wybierze reguły z pasujących katalogów R8. Jeśli biblioteka nie określa reguł kompresji, wtyczka Gradle na 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 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
. Poniższy plik Gradle dodaje flavor2-rules.pro
do wersji produktu flavor2
.
Teraz flavor2
wykorzystuje wszystkie 3 reguły ProGuard, ponieważ stosowane są również te z blokady release
.
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 zredukować 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 Androida może używać do otwierania aktywnoś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()
i 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.
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 kompresji 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
) wystarcza do usunięcia przez R8 tylko nieużywanego kodu. Jednak w niektórych sytuacjach R8 może mieć trudności z prawidłowym przeanalizowaniem kodu i może usunąć kod, którego aplikacja faktycznie potrzebuje. 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 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-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 @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 dołączony do wtyczki 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 aplikacji przeznaczonych do publikacji 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. 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 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. - 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ę 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
Skompresuj zawartość tego katalogu:
cd app/build/intermediates/cmake/universal/release/obj
zip -r symbols.zip .
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 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.
Dostosowywanie zasobów do zachowania
Jeśli chcesz zachować lub odrzucić określone 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 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. 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ą ze sobą połączone, ich reguły przechowywania będą ze sobą kolidować, co może spowodować potencjalne problemy z ignorowanymi regułami lub niepotrzebnie przechowywanymi 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 prawdziwym zasobem odwołującym się do zasobu 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
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 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());
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 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 też odwoływać się 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
Narzędzie Gradle do zmniejszania zasobów 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 do obsługi 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óre nie zostały określone, 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")) } }
Odlotowe
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 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.
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. 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ó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 kompilacji występuje duplikat zasobu, Gradle wybierze ten z wersji kompilacji.
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.
Zaciemnianie 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. 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 zrozumieć zrzut stosu po zaciemnieniu, przeczytaj sekcję Dekodowanie zaciemnionego zrzutu stosu.
Jeśli kod korzysta z przewidywalnych nazw 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 kodu do zachowania. Te reguły informują R8, aby nie tylko zachować ten kod w pliku DEX końcowej wersji 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 ś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 wybiera gałęzi
else {}
w danym poleceniu if/else, R8 może usunąć kod z gałęzielse {}
. - 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 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 na temat 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
i -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ę zrzutów stosu Twojej aplikacji. Na przykład wstawianie kodu usunie ramki stosu. Aby dowiedzieć się, jak uzyskać oryginalne ścieżki z wykresu wywołań, 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 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łączanie ulepszonych optymalizacji
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 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 wykonywania (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 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łę keep.
Jeśli napotkasz problemy podczas korzystania z „trybu pełnego” R8, odwiedź stronę z najczęstszymi pytaniami dotyczącymi R8, aby znaleźć możliwe rozwiązanie. Jeśli nie uda Ci się 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 obrysu. Największym czynnikiem jest 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 wartość SourceFile
. Rzeczywista nazwa oryginalnego pliku źródłowego nie jest wymagana przy przywracaniu, ponieważ plik mapowania zawiera pierwotny 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 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 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 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 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 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 punktów wejścia określonych przez R8 na podstawie reguł zachowania w Twoim projekcie , dodaj do pliku reguł niestandardowych 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). Na przykład:
:android:shrinkDebugResources
Removed unused resources: 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). 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 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
Musisz teraz wiedzieć, dlaczego zasób @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 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, które mają być zachowywane.