Optymalizacja kodu i zasobów platformy za pomocą R8

System kompilacji platformy Android (Soong) uruchamia kompilator R8 na elementach docelowych, które kompilują kod bajtowy DEX, w tym aplikacje na Androida (android_app), testy (android_test) i biblioteki Java z możliwością zainstalowania z compile_dex: true (np. services.jar), aby zmniejszyć rozmiar, zoptymalizować i usunąć nieużywany kod i zasoby. W przypadku bibliotek statycznych (android_library i statycznych java_library) Soong nie uruchamia R8 bezpośrednio, ale używa bloku właściwości optimize, aby dołączać i propagować reguły zachowywania konsumenta (export_proguard_flags_files: true) do docelowych plików DEX, które są z nimi statycznie połączone.

W przypadku inżynierów platformy tworzących obrazy systemu lub pakiety dostawców wyeliminowanie zbyt ogólnych reguł zachowywania przynosi bezpośrednie korzyści dla kondycji systemu:

  • Mniejszy rozmiar partycji systemowej: R8 usuwa nieużywane klasy, metody i zasoby przed spakowaniem plików APK i JAR na /system, /system_ext, /product i /vendor.
  • Mniejsze skompilowane artefakty: mniej metod DEX oznacza mniejsze pliki .odex i.vdex generowane przez dex2oat podczas kompilacji w czasie kompilacji lub na urządzeniu.
  • Mniejsze obciążenie pamięci w czasie działania: ponieważ Android mapuje tabele zasobów .odex, .vdex i APK do pamięci procesu za pomocą wywołań mmap z pamięcią stronicowaną na żądanie, mniejsze pliki binarne ograniczają poważne błędy strony podczas uruchamiania aplikacji i zmniejszają pamięć kodu rezydentnego i zasobów w procesach. Więcej informacji o tym, jak skompilowany kod aplikacji wpływa na pamięć urządzenia, znajdziesz w artykule Kod aplikacji to pamięć.
  • Skuteczniejsza optymalizacja całego programu: wąskie ograniczenia dotyczące zachowywania umożliwiają R8 wstawianie metod, devirtualizację wywołań interfejsów i wywołań wirtualnych, usuwanie nieużywanych pól i propagowanie stałych w klasach.

Konfigurowanie optymalizacji R8 w Soong

W plikach Android.bp skonfiguruj optymalizację R8 za pomocą bloku właściwości optimize w przypadku celów android_app i instalowanych java_library (lub w przypadku modułów android_library i statycznych java_library, aby wyeksportować reguły zachowywania dla konsumentów):

android_app {
    name: "MySystemApp",
    srcs: ["src/**/*.java"],
    optimize: {
        obfuscate: true,
        shrink_resources: true,
    },
}

W tabeli poniżej znajdziesz podsumowanie najczęstszych właściwości optimize w Soong (zdefiniowanych w build/soong/java/dex.go):

Właściwość Opis
enabled Określa, czy R8 ma być uruchamiany na urządzeniu docelowym. Domyślna wartość to true w przypadku wszystkich android_app.
shrink Steruje usuwaniem nieużywanych klas, pól i metod. Domyślnie przyjmuje wartość true w przypadku android_app elementów docelowych (false w przypadku samodzielnych java_library i modułów testowych, które przekazują -dontshrink, chyba że zostanie to wyraźnie ustawione).
optimize Określa optymalizacje kodu bajtowego, takie jak wstawianie metod, scalanie klas, propagacja stałych i usuwanie nieużywanych gałęzi. Wartość domyślna to true w przypadku android_app (kontrolowana przez flagę kompilacji do publikacji RELEASE_R8_OPTIMIZE_BY_DEFAULT).
obfuscate Kontroluje minifikację i zmianę nazw identyfikatorów. Domyślnie jest ustawiona wartość false ze względu na zgodność historyczną, ale w miarę możliwości ustawiaj wartość true, aby zmniejszyć rozmiar pliku DEX i odblokować bardziej zaawansowane optymalizacje (patrz Włączanie zaciemniania w miarę możliwości).
shrink_resources Usuwa nieużywane zasoby (wpisy res/) z spakowanego pliku APK po zmniejszeniu kodu. Domyślna wartość to false.
optimized_shrink_resources Uruchamia zintegrowany potok R8 do zmniejszania kodu i zasobów, dzięki czemu R8 śledzi kod i odwołania do zasobów w jednym przebiegu. Gdy ustawiona jest wartość shrink_resources: true, domyślnie używana jest wartość RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT (true w przypadku standardowych wersji platformy).
proguard_flags_files Zawiera listę plików .flags lub .pro specyficznych dla modułu, które zawierają niestandardowe reguły przechowywania. Unikaj dodawania plików niestandardowych, jeśli wystarczą standardowe adnotacje lub wartości domyślne.
export_proguard_flags_files Przekazuje poziom proguard_flags_files tej biblioteki do modułów podrzędnych, które są od niej statycznie zależne.
trace_references_from Zawiera listę celów towarzyszących bibliotece Java, których kod bajtowy odwołuje się do tego celu. R8 automatycznie śledzi i zachowuje te cele.

Włączaj zaciemnianie wszędzie tam, gdzie to możliwe.

W Soong domyślnie używana jest wartość obfuscate false, aby zachować zgodność z wcześniejszymi wersjami, ale w miarę możliwości należy jawnie ustawić wartość obfuscate: true w przypadku android_app i samodzielnych elementów DEX:

  • Mniejszy DEX i .vdex rozmiar: zmiana nazw pakietów, klas, pól i metod na krótkie identyfikatory (a, b) zmniejsza pulę ciągów DEX (string_ids i string_data_item) oraz deskryptory typów. Ponieważ Android mapuje pliki .vdex i .odex do pamięci procesu, mniejsze tablice symboli bezpośrednio zmniejszają rozmiar partycji systemowej i wykorzystanie pamięci w czasie działania.
  • Bardziej zaawansowane optymalizacje całego programu: umożliwienie R8 zmiany nazw identyfikatorów odblokowuje scalanie klas, spłaszczanie pakietów i usuwanie duplikatów klas syntetycznych w pakietach, które w przeciwnym razie R8 musiałby pominąć, aby uniknąć konfliktów nazw.
  • Symbolizacja pełnego zrzutu stosu: włączenie zaciemniania w Soong nie zmniejsza możliwości debugowania. W przypadku każdego skompilowanego elementu docelowego R8 Soong generuje plik mapowania proguard_dictionary w katalogu pośrednim modułu, pakuje wszystkie słowniki modułu do artefaktu proguard-dict.zip kompilacji, osadza w nagłówku DEX skrót mapowania (--map-id-template) i przepisuje atrybuty klasy SourceFile (--source-file-template), aby retrace mogło automatycznie symbolizować ślady stosu.

Zachowaj obfuscate: false (lub wyraźnie zachowaj nazwy publicznych interfejsów API) tylko w przypadku:

  • Biblioteki współdzielone na ścieżce rozruchowej lub ścieżce klasy system_server: moduły (java_library lub java_sdk_library), które udostępniają zewnętrzny interfejs API, są łączone dynamicznie przez inne moduły w czasie wykonywania (np. elementy docelowe framework.jar, services.jar lub <uses-library>). Muszą one zachować obfuscate: false lub jawnie zachować swój interfejs API za pomocą reguł @KeepForApi lub -keep (wraz z protect_api_surface: true w przypadku elementów docelowych ścieżki rozruchowej), aby wywołujący skompilowani na podstawie ich stubów mogli rozwiązywać nazwy klas i elementów w czasie wykonywania.

Zanim włączysz obfuscate: true w aplikacji, sprawdź, czy spełniasz te wymagania wstępne migracji:

  • Zależności zewnętrzne testowego pliku APK: jeśli zewnętrzny android_test zestaw plików APKinstrumentation_for: "MyApp" bezpośrednio wywołuje wewnętrzne klasy lub metody MyApp, zmiana nazw tych symboli powoduje NoSuchMethodError lub NoClassDefFoundError w czasie działania testu. Zalecamy skonfigurowanie testu jako samodzielnie instrumentującego pliku APK, który MyApp.impl statycznie łączy, dodanie do punktów zaczepienia testu adnotacji @VisibleForTesting lub skonfigurowanie trace_references_from (patrz krok 2. Migracja reguł zachowywania opartych na testach), aby wewnętrzne symbole potrzebne do testów były zachowywane, a pozostała część aplikacji była zaciemniana.
  • Odbicie oparte na ciągach znaków i JNI: jeśli moduł wyszukuje klasy, metody lub pola według dosłownych nazw ciągów znaków (Class.forName, getDeclaredMethod lub JNI FindClass i GetMethodID) bez reguł zachowywania lub adnotacji, zaciemnianie zmienia nazwy tych elementów i przerywa wyszukiwanie w czasie działania. (Pamiętaj, że nieoznaczone odbicie również nie działa w przypadku shrink: true i optimize: true). Oznacz te punkty wejścia za pomocą Przewodnika po adnotacjach Keep (@UsesReflection, @UsedByReflection lub @UsedByNative), aby R8 zachował ich nazwy i zaciemnił resztę modułu.

Stosuj zasadę braku niestandardowych flag

Idealny moduł platformy Android nie zawiera niestandardowych plików proguard.flags ani keep.xml. W kompilacji platformy Android większość niestandardowych reguł zachowywania jest zbędna:

  • Podstawowe ustawienia platformy i AAPT2: większość punktów wejścia jest już automatycznie zachowywana przez globalne podstawowe ustawienia platformy (np. @Keep, metody JNI native i @VisibleForTesting) lub generowana przez AAPT2 na podstawie AndroidManifest.xml i zasobów układu (więcej informacji znajdziesz w artykule Wyjaśnienie domyślnych reguł zachowywania platformy).
  • Alternatywne rozwiązania ukierunkowane: w przypadku konieczności zachowania punktów wejścia preferuj adnotacje w pliku deklaracji (keepanno, @VisibleForTesting), wyeksportowane reguły biblioteki (export_proguard_flags_files: true) lub statyczne łączenie testów zamiast odłączonych plików .flags (patrz Sprawdzanie i migracja istniejących reguł zachowywania).

Zanim dodasz lub zachowasz niestandardowy plik proguard.flags lub keep.xml, sprawdź, czy reguła jest już obsługiwana przez domyślne wartości bazowe lub czy można ją przenieść do adnotacji w kodzie.

Domyślne reguły przechowywania na platformie

Soong automatycznie przekazuje te globalne reguły podstawowe do każdego wywołania R8 w przypadku docelowych elementów kompilacji DEX (android_app, android_test i instalowane moduły java_library) skonfigurowanych w build/soong/java/dex.go:

  • build/make/core/proguard.flags: zachowuje klasy i elementy oznaczone adnotacją @com.android.internal.annotations.VisibleForTesting we wszystkich pakietach oraz oznaczone adnotacją @VisibleForTesting (androidx.annotation.VisibleForTesting lub com.google.common.annotations.VisibleForTesting) w pakietach android.**, com.android.** i com.google.android.**. Zachowuje też @TestApi, @Keep (androidx.annotation, android.support.annotation, com.android.internal.annotations), @KeepForWeakReference, @WeaklyReferencedCallback i @dalvik.annotation.optimization.**.
  • build/make/core/proguard_basic_keeps.flags: zachowuje atrybuty SourceFile zrzutów stosu, adnotacje dotyczące widoczności w czasie działania (RuntimeVisible*Annotations), atrybuty Exceptions i AnnotationDefault, metody native, elementy Serializable, metody @JavascriptInterface, konstruktory Throwable(String), pola Parcelable$CREATOR i pola MessageLite protokołu protobuf.
  • build/make/core/proguard/kotlin.flags: wycisza nieszkodliwe ostrzeżenia dotyczące określonych metaanotacji języka Kotlin (kotlin.Metadata i kotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target}) oraz usuwa anotacje DebugMetadata w wersjach produkcyjnych.
  • build/make/core/proguard/checknotnull.flags: zastępuje typowe wywołania pomocnicze sprawdzające wartość null (com.google.common.base.Preconditions.checkNotNull i dagger.internal.Preconditions.checkNotNull*) zwięzłymi sprawdzaniami wartości null w kodzie bajtowym. W tym pliku celowo pominięto Objects.requireNonNull, aby zachować wyraźne komunikaty o wyjątkach w ramach granic interfejsu API platformy.
  • build/make/core/proguard/enumvalues.flags: zachowuje metody values i valueOf w przypadku typów enum, chyba że zostaną wyłączone przez konfigurację kompilacji.
  • Automatycznie generowane reguły AAPT2: AAPT2 sprawdza scalone pliki AndroidManifest.xml, XML układu i XML preferencji, aby wygenerować dokładne reguły zachowywania dla każdego zarejestrowanego elementu Activity, Service, BroadcastReceiver, ContentProvider, BackupAgent, Application, niestandardowej podklasy View, podklasy Preference i metody XML android:onClick.

Sprawdzanie i przenoszenie dotychczasowych reguł przechowywania

Podczas sprawdzania istniejących plików proguard.flags lub keep.xml w repozytorium platformy oceń każdą regułę w kolejności zgodnie z tą 4-stopniową hierarchią:

Krok 1. Usuń zbędne lub przestarzałe reguły

Usuń reguły, które są już objęte globalnymi wartościami bazowymi, regułami manifestu i układu AAPT2 lub domyślnymi ustawieniami R8, a także reguły odwołujące się do klas lub pakietów, które już nie istnieją.

Jeśli wszystkie reguły w pliku .flags są zbędne, usuń plik i usuń proguard_flags_files z Android.bp. Jeśli pozostały blok optimize tylko powtarza ustawienia domyślne, usuń go i sformatuj plik kompilacji za pomocą bpfmt -w Android.bp. Podczas czyszczenia pakietu sprawdź, czy moduły towarzyszące w podkatalogach (np. warianty docelowe Kotlin) odwołują się do tego samego pliku flag i zaktualizuj je razem.

Krok 2. Przenieś reguły przechowywania oparte na testach

Przenieś reguły przechowywania oparte na testach z niestandardowych plików produkcyjnych .flags, używając jednego z tych wzorców:

  • Połącz bibliotekę implementacji z testami (static_libs): gdy test android_test korzysta ze szczegółów implementacji aplikacji, które są prywatne w pakiecie lub wewnętrzne, zalecana architektura platformy polega na umieszczeniu plików źródłowych aplikacji w android_library (MyApp.impl) i statycznym połączeniu MyApp.impl z MyApp i MyAppTests:

    android_library {
        name: "MyApp.impl",
        srcs: ["src/**/*.java"],
        manifest: "AndroidManifest.xml",
    }
    
    android_app {
        name: "MyApp",
        static_libs: ["MyApp.impl"],
        optimize: {
            obfuscate: true,
            shrink_resources: true,
        },
    }
    
    android_test {
        name: "MyAppTests",
        srcs: ["tests/**/*.java"],
        static_libs: ["MyApp.impl"],
    }
    

    Ten 3-modułowy wzorzec umożliwia uruchamianie modułu MyAppTests jako testu z samodzielnym instrumentowaniem z pełnym dostępem do klas wewnętrznych, pozwala modułowi MyApp swobodnie włączać moduł obfuscate: true bez przerywania testów, zapobiega wysyłaniu punktów wejścia tylko do testów w APK produkcyjnym i eliminuje konieczność ponownego kompilowania modułu MyApp po zmianie kodu testowego.

  • Dodaj adnotacje do punktów zaczepienia testu za pomocą @VisibleForTesting: jeśli zewnętrzny plik APK testu wywołuje niewielką liczbę wewnętrznych metod lub konstruktorów w  android_app i restrukturyzacja w bibliotekę .impl jest niepraktyczna, dodaj do tych deklaracji adnotacje @VisibleForTesting. Globalna build/make/core/proguard.flagslinia bazowa zachowuje@VisibleForTesting elementy w pakietach android.**, com.android.** i com.google.android.** automatycznie bez niestandardowych .flagsplików. (W przypadku modułów dostawców spoza tych przestrzeni nazw używaj reguł @UsedByReflection lub wyeksportowanych bibliotek).

  • Używaj trace_references_from w różnych bibliotekach platformy: jeśli testy nie mogą statycznie połączyć implementacji docelowej (np. testy korzystające z usług serwera systemowego lub plików JAR platformy, takich jak framework-connectivity), skonfiguruj trace_references_from w module docelowym, aby wskazywał moduł towarzyszący biblioteki Java zawierający źródła testowe. R8 śledzi i zachowuje wszystkie klasy i elementy, do których odwołuje się kod bajtowy towarzyszący.

Krok 3. Eksportowanie reguł z biblioteki będącej właścicielem

Jeśli biblioteka współdzielona java_library lub android_library wymaga reguł przechowywania na potrzeby własnych wewnętrznych odbić lub wywołań zwrotnych JNI, zdefiniuj reguły w przypadku docelowej biblioteki i ustaw export_proguard_flags_files: true:

java_library {
    name: "my-shared-library",
    srcs: ["src/**/*.java"],
    optimize: {
        proguard_flags_files: ["proguard.flags"],
        export_proguard_flags_files: true,
    },
}

Wszystkie dalsze cele android_app i cele biblioteki, które są statycznie połączonemy-shared-library, automatycznie dziedziczą te reguły, więc aplikacje podrzędne nie muszą ich duplikować. Jeśli wiele modułów w różnych katalogach musi współdzielić zestaw reguł, który nie jest powiązany z jedną biblioteką kodu, opublikuj plik .flags za pomocą jawnego filegroup w Android.bp, aby moduły mogły się odwoływać do :my-shared-flags w sposób przejrzysty w ramach granic pakietu. Ogólne wskazówki dotyczące tworzenia reguł przechowywania konsumenta biblioteki bez ograniczania optymalizacji aplikacji podrzędnej znajdziesz w artykule Optymalizacja dla autorów bibliotek.

Krok 4. Dodaj adnotacje do deklaracji w kodzie źródłowym

Gdy klasa, metoda, konstruktor lub pole są dostępne przez odbicie lub JNI i nie są objęte AAPT2 ani globalnymi wartościami bazowymi, zastąp odłączone reguły -keep w plikach .flags adnotacjami źródłowymi z przewodnika po adnotacjach Keep (patrz keepanno dokumentacja Javadoc):

  • @UsesReflection: używaj tej adnotacji, gdy masz kontrolę nad kodem wykonującym odbicie. Umieść go w miejscu wywołania refleksji, aby zadeklarować, do których klas, metod lub pól docelowych uzyskiwany jest dostęp dynamiczny. Ponieważ adnotacja @UsesReflection automatycznie koduje warunek wstępny, że sam adnotowany punkt wywołania jest osiągalny, R8 usuwa zarówno wywołującego, jak i odzwierciedlony cel, jeśli punkt wywołania nie jest używany.
  • @UsedByReflection: umieść w klasach, metodach, polach lub konstruktorach, które są tworzone lub wywoływane przez kod lub biblioteki zewnętrzne, np. klasy wczytywane za pomocą Class.forName z kluczy Bundle lub Settings albo klasy wtyczek wczytywane w dynamicznych programach wczytujących klasy. Określ kind (np. KeepItemKind.CLASS_AND_METHODS), preconditions (@KeepCondition) i ograniczenia parametrów, aby R8 zachowywał tylko dokładny kontrakt odbicia i mógł nadal optymalizować lub usuwać nieużywane elementy klasy. Nie dodawaj @UsedByReflection do komponentów zarejestrowanych w pliku manifestu, takich jak JobService czy BroadcastReceiver, ponieważ AAPT2 już je przechowuje.
  • @UsedByNative: umieść w metodach lub polach, do których uzyskuje się dostęp z kodu JNI w C lub C++ za pomocą GetMethodID, GetStaticMethodID lub GetFieldID.
  • @KeepForApi: umieść w klasach lub elementach interfejsu API biblioteki, które muszą pozostać nienaruszone, gdy biblioteka zostanie zmniejszona przed dystrybucją.
  • @Keep (androidx.annotation.Keep): użyj jako wartości domyślnej, gdy keepanno nie ma zastosowania. Zastosowanie @Keep do klasy powoduje bezwarunkowe zachowanie klasy i wszystkich jej elementów. Zastosowanie adnotacji @Keep do metody lub pola działa jako bezwarunkowy punkt wejścia, który zachowuje element i jego klasę zawierającą, nawet jeśli klasa nigdy nie jest tworzona, natomiast adnotacja keepanno wyraża warunkową dostępność.

Aby używać adnotacji R8 keepanno w module Soong:

  1. Dodaj "keepanno-annotations" do libs w Android.bp:

    libs: [
        "keepanno-annotations",
    ],
    
  2. Zaimportuj com.android.tools.r8.keepanno.annotations.* i dodaj adnotację do deklaracji lub miejsca wywołania w kodzie źródłowym w Javie lub Kotlinie:

    import com.android.tools.r8.keepanno.annotations.KeepItemKind;
    import com.android.tools.r8.keepanno.annotations.UsedByReflection;
    
    public final class CustomPluginController {
        @UsedByReflection(
            description = "Instantiated via Class.forName from plugin config",
            kind = KeepItemKind.CLASS_AND_METHODS)
        public CustomPluginController(Context context) {
            // ...
        }
    }
    

    R8 tłumaczy adnotacje keepanno bezpośrednio na wewnętrzny model reguł zachowywania i usuwa adnotacje z końcowych danych wyjściowych DEX, dodając zero narzutu na kod bajtowy w czasie działania.

Unikanie typowych pułapek

W sekcjach poniżej opisujemy częste pułapki w kodzie platformy proguard.flags i keep.xml oraz sposoby ich eliminowania.

Ogólne reguły dotyczące przechowywania komponentów

# Don't do this:
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
  • Dlaczego obniża skuteczność: ta reguła jest całkowicie zbędna w przypadku AAPT2. Ponieważ używa symbolu wieloznacznego bez sprawdzania, czy komponent jest zarejestrowany w ostatecznej scalonej AndroidManifest.xml, wymusza na R8 zachowanie każdej podklasy Activity, Service lub BroadcastReceiver znalezionej w dowolnym miejscu ścieżki klasy, w tym nieużywanych komponentów biblioteki i wyłączonych działań debugowania.
  • Zalecane rozwiązanie: usuń regułę. AAPT2 sprawdza manifest łączony i generuje dokładne reguły -keep dla zarejestrowanych komponentów.

Reguły zachowywania przybliżonego modułu obsługi kliknięć XML

# Don't do this:
-keepclassmembers class * {
    public void *(android.view.View);
}
  • Dlaczego to obniża wydajność: zachowanie każdej metody public void *(View) w każdej klasie w module uniemożliwia R8 usunięcie lub wstawienie dowolnej metody, która akceptuje pojedynczy parametr View.
  • Zalecane rozwiązanie: usuń regułę. Narzędzie AAPT2 skanuje pliki XML układu i generuje ukierunkowane reguły zachowywania dla metod, do których odwołują się atrybuty android:onClick.

Reguły zachowywania funkcji pobierających i ustawiających w widoku ogólnym

# Don't do this:
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(...);
    public *** get*();
}
  • Dlaczego ta reguła obniża wydajność: ta reguła wymusza zachowanie przez R8 wszystkich metod pobierających i ustawiających we wszystkich podklasach View w aplikacji i we wszystkich połączonych bibliotekach (w tym bibliotekach AndroidX i Material), co uniemożliwia usuwanie nieużywanych metod i wstawianie kodu w kodzie interfejsu.
  • Zalecane rozwiązanie: usuń regułę. AAPT2 zachowuje już konstruktory w przypadku niestandardowych klas View wczytywanych z plików XML układu. Jeśli kod animuje właściwość widoku za pomocą nazw ciągów znaków opartych na odbiciu, takich jak ObjectAnimator.ofFloat(view, "translationZ", ...), zastąp nazwę ciągu znaków odwołaniem do właściwości z określonym typem (View.TRANSLATION_Z lub niestandardową implementacją FloatProperty albo IntProperty), aby całkowicie uniknąć odbicia. Jeśli nie można uniknąć dostępu do właściwości za pomocą refleksji, oznacz konkretny getter lub setter adnotacją @UsedByReflection.

Wyłączanie optymalizacji lub zaciemniania

# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
  • Dlaczego wpływa to na wydajność: umieszczenie -dontoptimize lub -dontshrink w pliku .flags cicho zastępuje ustawienia modułu Android.bp i wyłącza optymalizację w całym obszarze docelowym. Co gorsza, jeśli biblioteka eksportuje plik .flags zawierający -dontoptimize, wyłącza optymalizację R8 w przypadku każdego kolejnego modułu android_app, który łączy bibliotekę.
  • Zalecane rozwiązanie: usuń znaki -dontoptimize, -dontshrink i -dontobfuscate z plików .flags. Kontroluj zachowanie optymalizacji w Android.bp za pomocą bloku optimize (shrink, optimize, obfuscate) w przypadku docelowego elementu końcowego.

Flagi diagnostyczne w zatwierdzonych regułach

# Don't do this in proguard.flags:
-verbose
-printmapping proguard.map
-printusage usage.txt
-printconfiguration config.txt
  • Dlaczego to szkodzi kompilacji: flagi diagnostyczne zalewają dzienniki kompilacji lub próbują zapisywać pliki wyjściowe w lokalnych ścieżkach podczas kompilacji Soong w środowisku piaskownicy.
  • Zalecane rozwiązanie: usuń te flagi z zatwierdzonych plików .flags. Soong automatycznie zapisuje dane wyjściowe mapowania i użycia R8, takie jak proguard_dictionary i proguard_usage.zip, w katalogu pośrednim modułu w folderze out/soong/.intermediates/.

Reguły zachowywania w wersji produkcyjnej dotyczące kodu testowego

  • Dlaczego to obniża wydajność: dodanie niestandardowych reguł -keep wyłącznie na potrzeby testowego dostępu wymusza pozostawienie tego kodu w niezaszyfrowanej postaci i zachowanie go w kompilacjach produkcyjnych. Chociaż dodawanie adnotacji do metod za pomocą symbolu @VisibleForTesting lub konfigurowanie trace_references_from pozwala uniknąć ręcznego utrzymywania reguł -keep, te symbole są nadal obecne w binarnym pliku produkcyjnym.
  • Zalecane rozwiązanie: aby punkty wejścia przeznaczone tylko do testowania nie były uwzględniane w pliku APK wersji produkcyjnej, podziel aplikację na .impl bibliotekę i połącz ją z samodzielnie instrumentującym się testowym plikiem APK za pomocą static_libs, jak opisano w kroku 2: Przenieś reguły zachowywania oparte na testach.

Ogólne symbole wieloznaczne zasobów w pliku keep.xml

<!-- Don't do this in res/raw/keep.xml: -->
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@raw/*,@drawable/*,@string/*" />
  • Dlaczego wpływa to negatywnie na wydajność: symbole wieloznaczne obejmujące wszystkie elementy zachowują każdy zasób w pasującym typie w module i jego zależnościach, co niweczy zmniejszanie zasobów (shrink_resources: true) i zwiększa rozmiar pliku APK oraz mapowanej tabeli resources.arsc.
  • Zalecana poprawka:
    • Preferuj statyczne odwołania do zasobów zamiast keep.xml: unikaj używania metody Resources.getIdentifier do dynamicznego wyszukiwania ograniczonego zestawu zasobów, takich jak ponumerowane ciągi eksperymentów lub tematyczne elementy rysunkowe. Zamiast tego użyj instrukcji switch w czasie kompilacji lub mapowania na stałeR.id, R.string lub R.drawable. Statyczne odwołania umożliwiają R8 i AAPT2 śledzenie dokładnych zasobów na żywo, eliminowanie narzutu związanego z wyszukiwaniem ciągów znaków w czasie działania i usuwanie potrzeby stosowania keep.xml.
    • Wymień konkretne nazwy zasobów: jeśli wymagane jest dynamiczne wyszukiwanie, np. przez czytnik licencji zewnętrznego dostawcy, wymień dokładne identyfikatory zasobów w tools:keep (np. tools:keep="@raw/third_party_licenses").
    • Usuń zbędne pliki keep.xml: jeśli wymienione zasoby są już statycznie przywoływane w kodzie (R.raw.foo) lub pliku XML (@raw/foo), narzędzie do zmniejszania rozmiaru automatycznie je zachowa, więc możesz usunąć res/raw/keep.xml.

Sprawdzanie i audytowanie zmian w regułach

Za każdym razem, gdy usuwasz lub zawężasz reguły zachowywania w proguard.flags lub keep.xml, sprawdź, czy moduł jest prawidłowo kompilowany, przechodzi testy jednostkowe i testy instrumentacji oraz zachowuje wszystkie wymagane punkty wejścia.

Tworzenie i testowanie modułów

  1. Skompiluj moduł, aby sprawdzić, czy R8 i AAPT2 działają bez ostrzeżeń o brakujących odwołaniach:

    m <MODULE_NAME>
    
  2. Uruchom testy jednostkowe i testy z instrumentacją modułu za pomocą polecenia atest:

    atest <TEST_MODULE_NAME>
    
  3. W przypadku aplikacji systemowych, usług uprzywilejowanych lub modułów zależnych od sprzętu uruchamiaj testy instrumentacyjne i testy interfejsu na docelowych urządzeniach fizycznych lub w laboratorium testowym, aby przetestować ścieżki odbicia w czasie działania, powiązania IPC i inflację zasobów.

Sprawdzanie różnic w plikach DEX i zasobach

Porównaj skompilowany plik APK lub JAR przed i po zmianach reguł, aby potwierdzić, że R8 usuwa martwy kod i zasoby bez usuwania oczekiwanych punktów wejścia:

  • Użyj apkanalyzer lub dexdump, aby sprawdzić zachowane klasy, metody i pola w wyjściowych plikach APK lub DEX:

    apkanalyzer dex packages $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • Podczas modyfikowania keep.xml lub włączania shrink_resources użyj aapt2 dump resources, aby sprawdzić, czy kompilacja usuwa nieużywane zasoby i zachowuje wymagane:

    aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    

Analizowanie promienia zachowywania i podporządkowania reguł za pomocą R8

Kompilator R8 o otwartym kodzie źródłowym zawiera analizator Keep Radius (dostępny też w Android Studio i Gradle jako analizator konfiguracji R8), który mierzy dokładny wpływ każdej reguły zachowywania podczas kompilacji. Soong integruje ten analizator bezpośrednio z kompilacją platformy Android (skonfigurowaną w build/soong/java/dex.go).

Aby przeanalizować reguły zachowywania w przypadku modułu platformy lub całej kompilacji:

  1. Uruchom kompilację ze zmienną środowiskową R8_DUMP_KEEP_RADIUS=true:

    R8_DUMP_KEEP_RADIUS=true m <MODULE_NAME>
    

    Gdy ustawiona jest wartość R8_DUMP_KEEP_RADIUS=true, Soong instruuje R8, aby rejestrował dane reguł keep w pliku pośrednim r8keepradius.pb dla każdego skompilowanego modułu w folderze out/soong/.intermediates/. W przypadku każdej reguły zachowywania i keepanno adnotacji R8 rejestruje:

    • Promień natychmiastowego zachowania: dokładne klasy, pola i metody zachowane przez regułę wraz z określonymi ograniczeniami, które wymusza w zakresie zmniejszania, optymalizacji lub zaciemniania.
    • Podporządkowanie reguł: które inne reguły przechowywania lub adnotacje już zachowują te same elementy. Jeśli reguła niestandardowa jest w całości zawarta w regule AAPT2 lub globalnej regule podstawowej, możesz ją bezpiecznie usunąć.
    • Reguły dotyczące całego pakietu i reguły globalne: reguły, które używają szerokich symboli wieloznacznych obejmujących cały pakiet lub stosują globalne dyrektywy konfiguracyjne.
  2. Przekształć dane wyjściowe r8keepradius.pb w interaktywny raport HTML za pomocą narzędzia KeepRadiusHtmlReportGenerator (dołączonego do prebuilts/r8/r8.jar):

    # Generate an HTML report for a single module
    INTERMEDIATES=out/soong/.intermediates/packages/apps/<APP_NAME>/<APP_NAME>
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        $INTERMEDIATES/android_common/r8keepradius.pb \
        keep_radius_report.html
    
    # Scan all built modules under out/soong/.intermediates and generate
    # per-module HTML reports plus an aggregate keepradius.html summary
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        out/soong/.intermediates \
        out/keep_radius_reports
    

    Gdy podasz ścieżkę do katalogu, KeepRadiusHtmlReportGenerator przejdzie wszystkie*keepradius*.pb pliki, wygeneruje raport HTML dla każdego modułu i utworzy out/keep_radius_reports/keepradius.html podsumowanie liczby aktywnych elementów, liczby zachowanych elementów i reguł przechowywania o największym promieniu w całej kompilacji. Więcej informacji o interpretowaniu w wygenerowanym raporcie wyników dotyczących zmniejszania, optymalizacji i zaciemniania znajdziesz w artykule Korzystanie z analizatora konfiguracji R8.

Soong obsługuje też 2 dodatkowe zmienne środowiskowe diagnostyki R8 w build/soong/java/dex.go:

  • R8_DUMP_INPUT=true: zapisuje r8inputs.zip w katalogu pośrednim modułu zawierającym wszystkie wejściowe pliki JAR, pliki JAR biblioteki i scalane konfiguracje ProGuard na potrzeby samodzielnego odtwarzania R8.
  • R8_DUMP_PERFETTO_TRACE=true: zapisuje r8trace.ptrace w katalogu pośrednim modułu, aby umożliwić sprawdzanie przebiegów kompilacji R8 w Perfetto.

Weryfikowanie reguł dotyczących użytkowników biblioteki

Gdy eksporty java_library lub android_library przekazują reguły przechowywania do odbiorców niższego szczebla (export_proguard_flags_files: true), reguły te nie mogą zawierać globalnych flag, które zmieniają lub wyłączają optymalizację w aplikacji odbierającej.

R8 udostępnia analizator reguł zachowywania open source (ProcessKeepRules), który jest dostępny w platformie Android jako narzędzie hosta process-keep-rules (zdefiniowane w prebuilts/r8/Android.bp):

# Build the R8 keep rules validator host binary
m process-keep-rules

# Validate one or more ProGuard configuration files
out/host/linux-x86/bin/process-keep-rules <PROGUARD_FLAGS_PATH>

Narzędzie process-keep-rules analizuje każdy plik konfiguracyjny i w przypadku napotkania dyrektyw niedozwolonych w regułach dotyczących użytkowników biblioteki wyświetla diagnostykę pliku i wiersza. Niedozwolone dyrektywy obejmują optymalizację globalną, zmniejszanie lub wyłączanie zaciemniania (np. -dontoptimize lub -dontshrink), flagi ponownego pakowania i modyfikacji dostępu, flagi diagnostyczne lub mapowania oraz flagi -keepattributes na poziomie aplikacji.

Sprawdzanie drzew źródeł za pomocą skryptu pgaudit.py

Aby przeprowadzić audyt niekompilowanych katalogów źródłowych wraz z analizatorami czasu kompilacji R8, drzewo platformy Android zawiera skrypt pgaudit.py w katalogu build/make/core/proguard/tools/. To narzędzie do analizy statycznej skanuje pliki Android.bp, proguard.flags i keep.xml w repozytorium, aby oznaczać reguły, które są już objęte przez AAPT2 lub globalne platformy bazowe, szerokie symbole wieloznaczne, zastąpienia -dontoptimize i reguły zachowywania tylko na potrzeby testów:

# Audit a single package directory
./build/make/core/proguard/tools/pgaudit.py packages/apps/Provision/

# Scan a subsystem tree and write a structured JSON report
./build/make/core/proguard/tools/pgaudit.py packages/ --json=audit_report.json

Zespoły platformy i OEM mogą łączyć pgaudit.py, aby szybko triażować drzewo źródłowe, process-keep-rules, aby weryfikować wyeksportowane reguły biblioteki, oraz R8_DUMP_KEEP_RADIUS=true z KeepRadiusHtmlReportGenerator, aby mierzyć dokładny promień zachowania klasy, pola i metody każdej reguły w kompilacji.