Articles

praktyczne przykłady zasad ProGuard

Wojtek Kaliciński
Wojtek Kaliciński

Follow
Feb 20, 2018 · 9 min Czytaj

w moim poprzednim artykule wyjaśniłem, dlaczego każdy powinien używać ProGuard dla swoich aplikacji na Androida, jak go włączyć i jakie błędy można napotkać podczas robienia tego. Było wiele teorii, ponieważ uważam, że ważne jest, aby zrozumieć podstawowe zasady, aby być przygotowanym do radzenia sobie z potencjalnymi problemami.

w osobnym artykule mówiłem również o bardzo specyficznym problemie konfiguracji ProGuard do natychmiastowej kompilacji aplikacji.

w tej części chciałbym opowiedzieć o praktycznych przykładach zasad ProGuard na średniej wielkości przykładowej aplikacji: Plaid autorstwa Nicka Butchera.

Plaid okazał się świetnym przedmiotem do badania problemów ProGuard, ponieważ zawiera mieszankę bibliotek innych firm, które używają takich rzeczy jak przetwarzanie adnotacji i generowanie kodu, odbicie, ładowanie zasobów Javy i kod natywny (JNI). Wyodrębniłem i zapisałem kilka praktycznych porad, które powinny mieć zastosowanie do innych aplikacji w ogóle:

klasy danych

public class User {
String name;
int age;
...
}

prawdopodobnie każda aplikacja ma jakąś klasę danych (znaną również jako DMO, modele itp. w zależności od kontekstu i miejsca, w którym znajdują się w architekturze aplikacji). Rzecz o obiektach danych jest taka, że zazwyczaj w pewnym momencie będą one ładowane lub zapisywane (serializowane) na innym nośniku, takim jak sieć (żądanie HTTP), baza danych (przez ORM), plik JSON na dysku lub w magazynie danych Firebase.

wiele narzędzi upraszczających serializację i deserializację tych pól opiera się na odbiciu. GSON, Retrofit, Firebase-wszystkie sprawdzają nazwy pól w klasach danych i zamieniają je w inną reprezentację (na przykład: {"name”: "Sue”, "age”: 28}), zarówno dla transportu, jak i przechowywania. To samo dzieje się, gdy wczytują dane do obiektu Java — widzą parę klucz-wartość "name”:”John” I próbują zastosować ją do obiektu Java, wyszukując pole String name .

Wniosek: nie możemy pozwolić ProGuard zmienić nazwy lub usunąć żadnych pól w tych klasach danych, ponieważ muszą one pasować do serializowanego formatu. Jest to Bezpieczny zakład, Aby dodać@Keep adnotację na całej klasie lub regułę wieloznaczną na wszystkich modelach:

-keep class io.plaidapp.data.api.dribbble.model.** { *; }

Ostrzeżenie: Podczas testowania, czy aplikacja jest podatna na ten problem, można popełnić błąd. Na przykład, jeśli serializujesz obiekt do JSON i zapisujesz go na dysku w wersji N aplikacji bez odpowiednich reguł keep, zapisane dane mogą wyglądać następująco: {"a”: "Sue”, "b”: 28}. Ponieważ ProGuard zmienił nazwę pól na a I b, wszystko wydaje się działać, dane zostaną zapisane i załadowane poprawnie.

jednak po ponownym zbudowaniu aplikacji i wydaniu wersji N+1 aplikacji ProGuard może zdecydować o zmianie nazwy pól na inne, takie jakc Id. W rezultacie dane zapisane wcześniej nie zostaną załadowane.

musisz przede wszystkim upewnić się, że masz odpowiednie zasady keep.

kod Javy wywołany od strony natywnej (JNI)

domyślne pliki ProGuard Androida (zawsze należy je uwzględnić, mają naprawdę przydatne reguły) zawierają już regułę dla metod zaimplementowanych po stronie natywnej (-keepclasseswithmembernames class * { native <methods>; }). Niestety nie istnieje żaden sposób na utrzymanie kodu wywoływanego w przeciwnym kierunku: z JNI do Javy.

z JNI jest całkowicie możliwe skonstruowanie obiektu JVM lub wyszukanie i wywołanie metody na uchwycie JVM z kodu C / C++ i w rzeczywistości jedna z bibliotek używanych w Plaid robi to.

wniosek: ponieważ ProGuard może sprawdzać tylko klasy Javy, nie będzie wiedział o żadnych zastosowaniach, które zdarzają się w kodzie natywnym. Musimy wyraźnie zachować takie użycie klas i członków za pomocą@Keep adnotacji lub-keep reguły.

-keep, includedescriptorclasses 
class in.uncod.android.bypass.Document { *; }
-keep, includedescriptorclasses
class in.uncod.android.bypass.Element { *; }

otwieranie zasobów z JAR/APK

Android ma swój własny system zasobów i zasobów, który normalnie nie powinien stanowić problemu dla ProGuard. Jednak w zwykłej Javie istnieje inny mechanizm ładowania zasobów bezpośrednio z pliku JAR, a niektóre biblioteki innych firm mogą go używać nawet po skompilowaniu w aplikacjach na Androida (w takim przypadku będą próbować załadować z APK).

problem polega na tym, że zazwyczaj te klasy będą szukać zasobów pod własną nazwą pakietu (co przekłada się na ścieżkę do pliku w JAR lub APK). ProGuard może zmieniać nazwy pakietów podczas zaciemniania, więc po kompilacji może się zdarzyć, że klasa i jej plik zasobów nie są już w tym samym pakiecie w końcowym pliku APK.

aby zidentyfikować ładowanie zasobów w ten sposób, możesz wyszukać wywołania do Class.getResourceAsStream / getResourceI ClassLoader.getResourceAsStream / getResource w kodzie i we wszystkich bibliotekach innych firm, na których polegasz.

wniosek: powinniśmy zachować nazwę każdej klasy, która ładuje zasoby z APK za pomocą tego mechanizmu.

W Plaid są dwa — jeden w bibliotece OkHttp i jeden w Jsoup:

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-keepnames class org.jsoup.nodes.Entities

jak wymyślić reguły dla bibliotek stron trzecich

w idealnym świecie każda zależność, której używasz, dostarczyłaby wymagane reguły ProGuard w AAR. Czasami zapominają o tym lub publikują tylko słoiki, które nie mają znormalizowanego sposobu dostarczania reguł ProGuard.

w takim przypadku, zanim zaczniesz debugować swoją aplikację i wymyślać reguły, pamiętaj, aby sprawdzić dokumentację. Niektórzy autorzy biblioteki dostarczają zalecane reguły ProGuard (takie jak Retrofit używany w Plaid), które mogą zaoszczędzić dużo czasu i frustracji. Niestety, wiele bibliotek tego nie robi (jak to ma miejsce w przypadku jsoup i Bypass wymienionych w tym artykule). Należy również pamiętać, że w niektórych przypadkach konfiguracja dostarczona z biblioteką będzie działać tylko z wyłączonymi optymalizacjami, więc jeśli je włączysz, możesz znajdować się na terytorium uncharted.

więc jak wymyślić reguły, gdy biblioteka ich nie dostarcza?
mogę dać Ci tylko kilka wskazówek:

  1. przeczytaj wyjście kompilacji i logcat!
  2. Ostrzeżenia kompilacji powiedzą, które -dontwarn zasady dodawania
  3. ClassNotFoundExceptionMethodNotFoundException I FieldNotFoundException powiedzą, które -keep zasady dodawania

powinieneś się cieszyć, gdy Twoja aplikacja zawiesza się z włączonym ProGuard — będziesz miał gdzie zacząć swoje dochodzenie 🙂

najgorszą klasą problemów do debugowania jest, gdy aplikacja działa, ale na przykład nie wyświetla ekranu lub nie ładuje danych z sieci.

To jest miejsce, w którym musisz wziąć pod uwagę niektóre scenariusze, które opisałem w tym artykule i pobrudzić sobie ręce, nawet zanurzając się w kodzie strony trzeciej i rozumiejąc, dlaczego może się nie udać, na przykład gdy używa odbicia, introspekcji lub JNI.

debugowanie i śledzenie stosu

ProGuard domyślnie usunie wiele atrybutów kodu i ukrytych metadanych, które nie są wymagane do wykonania programu . Niektóre z nich są rzeczywiście przydatne dla programisty — na przykład, możesz chcieć zachować nazwy plików źródłowych i numery linii dla śladów stosu, aby ułatwić debugowanie:

-keepattributes SourceFile, LineNumberTable

należy również pamiętać, aby zapisać pliki mapowania ProGuard utworzone podczas tworzenia wersji wydania i przesłać je do Play, aby uzyskać de-zaciemnione ślady stosu z wszelkich awarii doświadczanych przez Twój komputer.użytkowników.

Jeśli masz zamiar dołączyć debugger, aby przejść przez kod metody w ProGuarded kompilacji aplikacji, należy również zachować następujące atrybuty, aby zachować pewne informacje debugowania o zmiennych lokalnych (potrzebujesz tylko tej linii wdebug typ kompilacji):

-keepattributes LocalVariableTable, LocalVariableTypeTable

minified debug build type

domyślne typy build są skonfigurowane tak, że Debug nie uruchamia ProGuard. Ma to sens, ponieważ chcemy szybko iterować i skompilować podczas programowania, ale nadal chcemy, aby Wersja release build używała ProGuard, aby była tak mała i zoptymalizowana, jak to tylko możliwe.

ale aby w pełni przetestować i debugować wszelkie problemy z ProGuard, dobrze jest skonfigurować oddzielną, minifikowaną kompilację debugowania w następujący sposób:

buildTypes {
debugMini {
initWith debug
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
matchingFallbacks =
}
}

dzięki temu typowi kompilacji będziesz mógł podłączyć debuger, uruchomić testy UI (również na serwerze CI) lub przetestować aplikację pod kątem możliwych problemów w kompilacji, która jest jak najbardziej zbliżona do kompilacji wydania.

wnioski: Kiedy używasz programu ProGuard, zawsze powinieneś dokładnie sprawdzać poprawność kompilacji, przeprowadzając testy end-to-end lub ręcznie przeglądając wszystkie ekrany w aplikacji, aby sprawdzić, czy czegoś brakuje lub nie ma.

adnotacje uruchomieniowe, wpisz introspekcję

ProGuard domyślnie usunie wszystkie adnotacje, a nawet niektóre dodatkowe informacje o typie z twojego kodu. W przypadku niektórych bibliotek nie stanowi to problemu — te, które przetwarzają adnotacje i generują kod w czasie kompilacji (takie jak Dagger 2 lub Glide i wiele innych) mogą nie potrzebować tych adnotacji później, gdy program działa.

istnieje inna klasa narzędzi, które faktycznie sprawdzają adnotacje lub sprawdzają informacje o typie parametrów i wyjątków w czasie wykonywania. Retrofit robi to na przykład przechwytując wywołania metody za pomocą obiektuProxy, a następnie przeglądając adnotacje i informacje typu, aby zdecydować, co umieścić lub odczytać z żądania HTTP.

wniosek: czasami wymagane jest zachowanie informacji o typie i adnotacji, które są odczytywane w czasie wykonywania, a nie w czasie kompilacji. Możesz sprawdzić listę atrybutów w podręczniku ProGuard.

-keepattributes *Annotation*, Signature, Exception

Jeśli używasz domyślnego pliku konfiguracyjnego Androida ProGuard (getDefaultProguardFile('proguard-android.txt')), dwie pierwsze opcje — adnotacje i podpis — są określone dla Ciebie. Jeśli nie używasz domyślnych, musisz upewnić się, aby dodać je samodzielnie (nie zaszkodzi również po prostu powielić je, jeśli wiesz, że są one wymagane dla Twojej aplikacji).

przeniesienie wszystkiego do domyślnego pakietu

opcja-repackageclasses nie jest domyślnie dodawana w konfiguracji ProGuard. Jeśli już zaciemniasz swój kod i naprawiłeś problemy z poprawnymi regułami keep, możesz dodać tę opcję, aby jeszcze bardziej zmniejszyć rozmiar DEX. Działa poprzez przeniesienie wszystkich klas do pakietu domyślnego (root), zasadniczo zwalniając przestrzeń zajmowaną przez ciągi takie jak ” com.przykład.myapp.somepackage”.

-repackageclasses

optymalizacje ProGuard

jak już wspomniałem, ProGuard może zrobić dla ciebie 3 rzeczy:

  1. pozbywa się nieużywanego kodu,
  2. zmienia nazwy identyfikatorów, aby Kod był mniejszy,
  3. wykonuje całe optymalizacje programu.

moim zdaniem każdy powinien spróbować skonfigurować swoją kompilację tak, aby uzyskać 1. i 2. pracuję.

aby odblokować 3. (dodatkowe optymalizacje), musisz użyć innego domyślnego pliku konfiguracyjnego ProGuard. Zmień proguard-android.txt parametr na proguard-android-optimize.txt w swoim build.gradle:

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

To sprawi, że Twoja wersja będzie wolniejsza, ale potencjalnie sprawi, że aplikacja będzie działać szybciej i zmniejszyć rozmiar kodu jeszcze bardziej, dzięki optymalizacje, takie jak inlining metod, scalanie klas i bardziej agresywne usuwanie kodu. Bądź jednak przygotowany, że może to wprowadzić nowe i trudne do zdiagnozowania błędy, więc używaj go ostrożnie, a jeśli coś nie działa, pamiętaj, aby wyłączyć pewne optymalizacje lub całkowicie wyłączyć korzystanie z konfiguracji optymalizacyjnej.

w przypadku Plaida optymalizacje ProGuard zakłócały sposób, w jaki Retrofit wykorzystuje Obiekty Proxy bez konkretnych implementacji, i pozbawiły niektóre parametry metody, które były faktycznie wymagane. Musiałem dodać ten wiersz do mojej konfiguracji:

-optimizations !method/removal/parameter

listę możliwych optymalizacji i jak je wyłączyć można znaleźć w podręczniku ProGuard.

kiedy używać @Keep I-keep

@Keep wsparcie jest faktycznie zaimplementowane jako kilka-keep reguł w domyślnym pliku reguł ProGuard Androida, więc są one zasadniczo równoważne. Podanie reguł-keep jest bardziej elastyczne, ponieważ oferuje symbole wieloznaczne, można również użyć różnych wariantów, które robią nieco inne rzeczy (-keepnames-keepclasseswithmembers I więcej).

ilekroć potrzebna jest prosta reguła „Zachowaj tę klasę” lub „Zachowaj tę metodę”, wolę prostotę dodawania@Keep adnotacji na klasie lub członie, ponieważ pozostaje ona blisko kodu, prawie jak dokumentacja.

Jeśli jakiś inny programista będzie chciał zrefaktorować kod, natychmiast dowie się, że Klasa/członek oznaczony @Keep wymaga specjalnej obsługi, bez konieczności pamiętania o sprawdzeniu konfiguracji ProGuard i ryzyku złamania czegoś. Również większość refaktoryzacji kodu w IDE powinna zachować adnotację@Keep z klasą automatycznie.

statystyki kratki

oto kilka statystyk z kratki, które pokazują, ile kodu udało mi się usunąć za pomocą ProGuard. W bardziej złożonej aplikacji z większą liczbą zależności i większym DEX oszczędności mogą być jeszcze większe.