Nowości dotyczące produktów

Konfigurowanie reguł zachowywania R8 i rozwiązywanie związanych z nimi problemów

7 min czytania
Wyświetl profil Ajesh Pai Wyświetl profil Bena Weissa
Ajesh Pai & Ben Weiss

W nowoczesnym tworzeniu aplikacji na Androida podstawowym oczekiwaniem użytkowników jest, aby aplikacja była mała, szybka i bezpieczna. Głównym narzędziem systemu kompilacji Androida do osiągnięcia tego celu jest optymalizator R8  – kompilator, który usuwa martwy kod i zasoby w celu zmniejszenia rozmiaru aplikacji, zmiany nazw kodu lub jego minifikacji oraz optymalizacji aplikacji.

Włączenie R8 jest kluczowym krokiem w przygotowaniu aplikacji do publikacji, ale wymaga od deweloperów podania wskazówek w postaci „reguł zachowywania”.

Po przeczytaniu tego artykułu obejrzyj na YouTube film z serii Performance Spotlight Week o włączaniu, debugowaniu i rozwiązywaniu problemów z optymalizatorem R8.

 

 

Dlaczego potrzebne są reguły zachowywania

Konieczność pisania reguł zachowywania wynika z podstawowego konfliktu: R8 to narzędzie do analizy statycznej, ale aplikacje na Androida często korzystają z dynamicznych wzorców wykonywania, takich jak odbicie lub wywołania do i z kodu natywnego za pomocą JNI (Java Native Interface).

R8 tworzy wykres używanego kodu, analizując bezpośrednie wywołania. Gdy kod jest dostępny w sposób dynamiczny, analiza statyczna R8 nie może tego przewidzieć i zidentyfikuje ten kod jako nieużywany i go usunie, co spowoduje awarie w czasie działania.

Reguła zachowywania to wyraźna instrukcja dla kompilatora R8: „Ta konkretna klasa, metoda lub pole jest punktem wejścia, do którego będzie się uzyskiwać dostęp dynamicznie w czasie działania. Musisz ją zachować, nawet jeśli nie możesz znaleźć do niej bezpośredniego odniesienia”.

Więcej informacji o regułach zachowywania znajdziesz w oficjalnym przewodniku.

Gdzie pisać reguły zachowywania

Niestandardowe reguły zachowywania aplikacji są zapisywane w pliku tekstowym. Zgodnie z konwencją ten plik ma nazwę proguard-rules.pro i znajduje się w katalogu głównym modułu aplikacji lub biblioteki. Ten plik jest następnie określany w rodzaju kompilacji release w pliku build.gradle.kts modułu.

release {

    isShrinkResources = true

    isMinifyEnabled = true

    proguardFiles(

        getDefaultProguardFile("proguard-android-optimize.txt"),

        "proguard-rules.pro",

    )

}

Używaj prawidłowego pliku domyślnego

Metoda getDefaultProguardFile importuje domyślny zestaw reguł dostarczonych przez Android SDK. Jeśli używasz nieprawidłowego pliku, aplikacja może nie być zoptymalizowana. Upewnij się, że używasz pliku proguard-android-optimize.txt. Ten plik zawiera domyślne reguły zachowywania standardowych komponentów Androida oraz włącza optymalizacje kodu R8. Przestarzały plik proguard-android.txt zawiera tylko reguły zachowywania, ale nie włącza optymalizacji R8.

progaurd.png

Ponieważ jest to poważny problem z wydajnością, zaczynamy ostrzegać deweloperów przed używaniem nieprawidłowego pliku. Pierwsze ostrzeżenia pojawią się w Android Studio Narwhal 3 Feature Drop.Od wersji 9.0 wtyczki Androida do obsługi Gradle nie obsługujemy już przestarzałego proguard-android.txt pliku. Dlatego upewnij się, że przechodzisz na zoptymalizowaną wersję.

Jak pisać reguły zachowywania

Reguła zachowywania składa się z 3 głównych części:

  1. Opcja , np. -keep lub -keepclassmembers
  2. Opcjonalne modyfikatory , np. allowshrinking
  3. Specyfikacja klasy , która definiuje kod do dopasowania

Pełną składnię i przykłady znajdziesz w przewodniku dodawania reguł zachowywania.

Antywzorce reguł zachowywania

Warto znać sprawdzone metody, ale też antywzorce. Te antywzorce często wynikają z nieporozumień lub skrótów w rozwiązywaniu problemów i mogą mieć katastrofalne skutki dla wydajności kompilacji produkcyjnej.

Opcje globalne

Te flagi to przełączniki globalne, których nigdy nie należy używać w kompilacji do publikacji. Służą one tylko do tymczasowego debugowania w celu wyizolowania problemu.

Użycie -dontotptimize skutecznie wyłącza optymalizacje wydajności R8, co powoduje spowolnienie aplikacji.

Użycie -dontobfuscate wyłącza wszystkie zmiany nazw, a użycie -dontshrink wyłącza usuwanie martwego kodu. Obie te reguły globalne zwiększają rozmiar aplikacji.

Aby zapewnić użytkownikom aplikacji lepszą wydajność, unikaj używania tych flag globalnych w środowisku produkcyjnym.

Zbyt ogólne reguły zachowywania

Najłatwiejszym sposobem na zniwelowanie korzyści z R8 jest napisanie zbyt ogólnych reguł zachowywania. Reguły zachowywania, takie jak ta poniżej, instruują optymalizator R8, aby nie zmniejszał rozmiaru, nie zaciemniał i nie optymalizował żadnej klasy w tym pakiecie ani żadnego z jego podpakietów. Całkowicie usuwa to korzyści z R8 dla całego pakietu. Zamiast tego spróbuj pisać wąskie i konkretne reguły zachowywania.

-keep class com.example.package.** { *;} // WIDE KEEP RULES CAUSE PROBLEMS

Operator negacji (!)

Operator negacji (!) wydaje się skutecznym sposobem na wykluczenie pakietu z reguły. Ale to nie takie proste. Spójrz na ten przykład:

-keep class !com.example.my_package.** { *; } // USE WITH CAUTION

Możesz pomyśleć, że ta reguła oznacza „nie zachowuj klas wcom.example.package.”. W rzeczywistości oznacza ona „zachowaj każdą klasę, metodę i właściwość w całej aplikacji, która nie znajduje się w com.example.package.”. Jeśli Cię to zaskoczyło, sprawdź, czy w konfiguracji R8 nie ma żadnych negacji.

Nadmiarowe reguły dotyczące komponentów Androida

Kolejnym częstym błędem jest ręczne dodawanie reguł zachowywania dla Activities, Services lub BroadcastReceivers aplikacji. Jest to niepotrzebne. Domyślny plik proguard-android-optimize.txt zawiera już odpowiednie reguły, które umożliwiają działanie tych standardowych komponentów Androida.

Wiele bibliotek ma też własne reguły zachowywania. Dlatego nie musisz pisać własnych reguł. Jeśli wystąpi problem z regułami zachowywania z używanej biblioteki, najlepiej skontaktować się z autorem biblioteki, aby dowiedzieć się, na czym polega problem.

Sprawdzone metody dotyczące reguł zachowywania

Wiesz już, czego nie robić, więc porozmawiajmy o sprawdzonych metodach.

Pisz wąskie reguły zachowywania

Dobre reguły zachowywania powinny być jak najwęższe i jak najbardziej szczegółowe. Powinny zachowywać tylko to, co jest niezbędne, aby R8 mógł zoptymalizować wszystko inne.

RegułaJakość

 

-keep class com.example.** { ; }

 

Niska: zachowuje cały pakiet i jego podpakiety

 

-keep class com.example.MyClass { ; }

 

Niska: zachowuje całą klasę, która prawdopodobnie jest nadal zbyt szeroka
-keepclassmembers class com.example.MyClass {

    private java.lang.String secretMessage;

    public void onNativeEvent(java.lang.String);

}
Wysoka: zachowuje tylko odpowiednie metody i właściwości z określonej klasy

Używaj wspólnych przodków

Zamiast pisać oddzielne reguły zachowywania dla wielu różnych modeli danych, napisz jedną regułę, która będzie kierowana na wspólną klasę bazową lub interfejs. Poniższa reguła nakazuje R8 zachowanie wszystkich elementów klas, które implementują ten interfejs, i jest wysoce skalowalna.

# Keep all fields of any class that implements SerializableModel

-keepclassmembers class * implements com.example.models.SerializableModel {

    <fields>;

}

Używaj adnotacji, aby kierować na wiele klas

Utwórz adnotację niestandardową (np. @Serialize) i użyj jej do „oznaczenia” klas, których pola mają być zachowane. Jest to kolejny czysty, deklaratywny i wysoce skalowalny wzorzec. Możesz też tworzyć reguły zachowywania dla już istniejących adnotacji z używanych przez Ciebie frameworków.

# Keep all fields of any class annotated with @Serialize

-keepclassmembers class * {

    @com.example.annotations.Serialize <fields>;

}

Wybierz odpowiednią opcję zachowywania

Opcja zachowywania jest najważniejszą częścią reguły. Wybranie nieprawidłowej opcji może niepotrzebnie wyłączyć optymalizację.

Opcja zachowywaniaCo robi
-keepZapobiega usunięciu lub zmianie nazwy klasy i elementów wymienionych w deklaracji .
-keepclassmembersZapobiega usunięciu lub zmianie nazwy określonych elementów, ale umożliwia usunięcie samej klasy, ale tylko w przypadku klas, które nie są usuwane w inny sposób.
-keepclasseswithmembersPołączenie: zachowuje klasę i jej elementy, tylko wtedy, gdy wszystkie określone elementy są obecne.

Więcej informacji o opcji zachowywania znajdziesz w naszej dokumentacji opcji zachowywania.

Zezwalaj na optymalizację za pomocą modyfikatorów

Modyfikatory takie jak allowshrinking i allowobfuscation rozluźniają ogólną regułę -keep, przywracając R8 możliwość optymalizacji. Jeśli np. starsza biblioteka wymusza użycie -keep w całej klasie, możesz odzyskać część optymalizacji, zezwalając na zmniejszanie rozmiaru i zaciemnianie:

# Keep this class, but allow R8 to remove it if it's unused and allow R8 to rename it.

-keep,allowshrinking,allowobfuscation class com.example.LegacyClass

Dodaj opcje globalne, aby uzyskać dodatkową optymalizację

Oprócz reguł zachowywania możesz dodać do pliku konfiguracyjnego R8 flagi globalne, aby zachęcić do jeszcze większej optymalizacji.

-repackageclasses to zaawansowana opcja, która instruuje R8, aby przeniósł wszystkie zaciemnione klasy do jednego pakietu. Pozwala to zaoszczędzić sporo miejsca w pliku DEX, usuwając zbędne ciągi nazw pakietów.

-allowaccessmodification umożliwia R8 rozszerzenie dostępu (np. z private na public), aby umożliwić bardziej agresywne wstawianie. Jest to teraz domyślnie włączone podczas używania proguard-android-optimize.txt.

Ostrzeżenie: autorzy bibliotek nigdy nie mogą dodawać tych globalnych flag optymalizacji do reguł konsumenta, ponieważ będą one wymuszane w całej aplikacji.

Aby było to jeszcze bardziej jasne, w wersji 9.0 wtyczki Androida do obsługi Gradle zaczniemy całkowicie ignorować globalne flagi optymalizacji z bibliotek. 

Sprawdzone metody dotyczące bibliotek

Każda aplikacja na Androida w taki czy inny sposób korzysta z bibliotek. Porozmawiajmy więc o sprawdzonych metodach dotyczących bibliotek.

Dla deweloperów bibliotek

Jeśli Twoja biblioteka używa odbicia lub JNI, musisz udostępnić jej użytkownikom niezbędne reguły zachowywania. Reguły te są umieszczane w pliku consumer-rules.pro, który jest następnie automatycznie dołączany do pliku AAR biblioteki.

android {

    defaultConfig {

        consumerProguardFiles("consumer-rules.pro")

    }

    ...

}

Dla użytkowników bibliotek

Filtruj problematyczne reguły zachowywania

Jeśli musisz używać biblioteki, która zawiera problematyczne reguły zachowywania, możesz je odfiltrować w pliku build.gradle.kts od wersji 9.0 AGP. Informuje to R8, aby ignorował reguły pochodzące z określonej zależności.

release {

    optimization.keepRules {

        // Ignore all consumer rules from this specific library

        it.ignoreFrom("com.somelibrary:somelibrary")

    }

}

Najlepsza reguła zachowywania to brak reguły zachowywania

Najlepszą strategią konfiguracji R8 jest całkowite wyeliminowanie potrzeby pisania reguł zachowywania. W przypadku wielu aplikacji można to osiągnąć, wybierając nowoczesne biblioteki, które preferują generowanie kodu zamiast odbicia. Dzięki generowaniu kodu optymalizator może łatwiej określić, który kod jest rzeczywiście używany w czasie działania, a który można usunąć. Ponadto brak użycia odbicia dynamicznego oznacza brak „ukrytych” punktów wejścia, a co za tym idzie, nie są potrzebne żadne reguły zachowywania. Podczas wybierania nowej biblioteki zawsze preferuj rozwiązanie, które używa generowania kodu zamiast odbicia.

Więcej informacji o tym, jak wybrać biblioteki, znajdziesz w artykule Wybieranie biblioteki.

Debugowanie i rozwiązywanie problemów z konfiguracją R8

Jeśli R8 usunie kod, który powinien zachować, lub jeśli plik APK jest większy niż oczekiwano, użyj tych narzędzi, aby zdiagnozować problem.

Znajdź zduplikowane i globalne reguły zachowywania

Ponieważ R8 scala reguły z kilkudziesięciu źródeł, trudno jest określić „ostateczny” zestaw reguł. Dodanie tej flagi do pliku proguard-rules.pro generuje pełny raport:

# Outputs the final, merged set of rules to the specified file

-printconfiguration build/outputs/logs/configuration.txt

Możesz przeszukać ten plik, aby znaleźć nadmiarowe reguły lub prześledzić problematyczną regułę (np. -dontoptimize) do konkretnej biblioteki, która ją zawiera.

Zapytaj R8: dlaczego to zachowujesz?

Jeśli klasa, która miała zostać usunięta, nadal znajduje się w aplikacji, R8 może Ci powiedzieć, dlaczego. Po prostu dodaj tę regułę:

# Asks R8 to explain why it's keeping a specific class

class com.example.MyUnusedClass

-whyareyoukeeping 

Podczas kompilacji R8 wyświetli dokładny łańcuch odniesień, które spowodowały zachowanie tej klasy, co pozwoli Ci prześledzić odniesienie i dostosować reguły.

Pełny przewodnik znajdziesz w sekcji Rozwiązywanie problemów z R8.

Dalsze kroki

R8 to zaawansowane narzędzie do zwiększania wydajności aplikacji na Androida. Jego skuteczność zależy od prawidłowego zrozumienia jego działania jako silnika analizy statycznej.

Pisząc konkretne reguły na poziomie elementów, wykorzystując przodków i adnotacje oraz starannie wybierając odpowiednie opcje zachowywania, możesz zachować dokładnie to, co jest niezbędne. Najbardziej zaawansowaną metodą jest całkowite wyeliminowanie potrzeby stosowania reguł przez wybranie nowoczesnych bibliotek opartych na generowaniu kodu zamiast ich poprzedników opartych na odbiciu.

W ramach Performance Spotlight Week obejrzyj dzisiejszy film z tej serii na YouTube i kontynuuj wyzwanie R8. Jeśli masz pytania dotyczące włączania R8 lub rozwiązywania problemów z nim, użyj tagu #optimizationEnabled. Chętnie Ci pomożemy.

Czas przekonać się o korzyściach.

Zachęcamy Cię, abyś dziś włączył w swojej aplikacji pełny tryb R8.

  1. Na początek zapoznaj się z naszymi przewodnikami dla deweloperów: Włączanie optymalizacji aplikacji.
  2. Sprawdź, czy nadal używasz pliku proguard-android.txt, i zastąp go plikiem proguard-android-optimize.txt.
  3. Następnie zbadaj wpływ. Nie tylko poczuj różnicę, ale też ją sprawdź. Aby zmierzyć wzrost wydajności, dostosuj kod z naszej przykładowej aplikacji Macrobenchmark na GitHubie , aby zmierzyć czasy uruchamiania przed i po.

Jesteśmy przekonani, że zauważysz znaczną poprawę wydajności aplikacji.

Jeśli masz pytania, użyj tagu #AskAndroid. Nasi eksperci będą przez cały tydzień monitorować Twoje pytania i na nie odpowiadać.

Wróć jutro, aby dowiedzieć się więcej o optymalizacji opartej na profilu z profilami bazowymi i profilami uruchamiania, o tym, jak w ostatnich wersjach poprawiła się wydajność renderowania w Compose, oraz o kwestiach związanych z wydajnością pracy w tle.

Autorzy:
Czytaj dalej