Optymalizacja kodu i zasobów platformy za pomocą R8

System kompilacji platformy Androida (Soong) uruchamia kompilator R8 na elementach docelowych, które kompilują kod bajtowy DEX, w tym na aplikacjach na Androida (android_app), testach (android_test) i bibliotekach Javy z możliwością zainstalowania z compile_dex: true (takich jak services.jar), aby zmniejszyć, zoptymalizować i usunąć nieużywany kod i zasoby. W przypadku bibliotek statycznych (android_library i statycznych java_library) Soong nie uruchamia bezpośrednio R8, ale używa bloku właściwości optimize do dołączania i rozpowszechniania reguł zachowywania konsumentów (export_proguard_flags_files: true) do dalszych elementów docelowych DEX, które łączą je statycznie.

Inżynierowie platformy, którzy tworzą obrazy systemu lub pakiety dostawców, mogą odnieść bezpośrednie korzyści z eliminacji zbyt szerokich reguł zachowywania:

  • Mniejszy rozmiar partycji systemowej: R8 usuwa nieużywane klasy, metody i zasoby przed spakowaniem plików APK i JAR do /system, /system_ext, /product i /vendor.
  • Mniejsze skompilowane artefakty: mniejsza liczba metod DEX oznacza mniejsze pliki .odex i .vdex generowane przez dex2oat podczas kompilacji lub kompilacji na urządzeniu.
  • Mniejsze obciążenie pamięci wykonawczej: ponieważ Android mapuje pliki .odex, .vdex i tabele zasobów pakietu APK na pamięć procesów za pomocą wywołań mmap stronicowania na żądanie, mniejsze pliki binarne zmniejszają liczbę poważnych błędów stron podczas uruchamiania aplikacji oraz zmniejszają ilość pamięci kodu 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ęć.
  • Bardziej efektywna optymalizacja całego programu: wąskie ograniczenia zachowywania pozwalają R8 na wstawianie metod, dewirtualizację interfejsu 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 obiektów docelowych android_app i java_library z możliwością zainstalowania (lub w przypadku modułów android_library i statycznych java_library, aby wyeksportować reguły zachowywania 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 działa w miejscu docelowym. Domyślna wartość to true dla wszystkich celów android_app.
shrink Steruje eliminacją martwego kodu, aby usunąć nieosiągalne klasy, pola i metody. Domyślnie true dla celów android_app (false dla samodzielnych modułów java_library i testowych, które przekazują -dontshrink, chyba że ustawiono inaczej).
optimize Kontroluje optymalizacje kodu bajtowego, takie jak wstawianie metod, scalanie klas, propagacja stałych i usuwanie martwych gałęzi. Domyślna wartość to true dla celów android_app (kontrolowana przez flagę kompilacji do publikacji RELEASE_R8_OPTIMIZE_BY_DEFAULT).
obfuscate Kontroluje minifikację i zmianę nazw identyfikatorów. Domyślnie jest to false ze względu na zgodność z poprzednimi wersjami, ale ustaw true, gdy tylko jest to możliwe, aby zmniejszyć rozmiar DEX i odblokować głębsze optymalizacje (patrz Włącz zaciemnianie kodu, gdy tylko jest to możliwe).
shrink_resources Usuwa nieużywane zasoby (res/ wpisy) z pakietu 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 odwołania do kodu i zasobów w jednym przebiegu. Gdy ustawiona jest wartość shrink_resources: true, domyślnie jest to RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT (true w standardowych kompilacjach platformy).
proguard_flags_files Zawiera listę plików .flags lub .pro powiązanych z modułem, które zawierają niestandardowe reguły zachowywania. Unikaj dodawania plików niestandardowych, gdy wystarczą standardowe adnotacje lub wartości domyślne.
export_proguard_flags_files Przekazuje proguard_flags_files tej biblioteki do modułów podrzędnych, które są od niej statycznie zależne.
trace_references_from Wyświetla listę bezpośrednich elementów docelowych zależnych od kompilacji DEX (połączonych przez instrumentation_for lub libs), których odwołania do kodu bajtowego w tym elemencie docelowym R8 automatycznie śledzi i zachowuje (są też propagowane w wariantach override_android_app i override_android_test oraz obsługiwane w przypadku niezależnych towarzyszących bibliotek Java, gdy są sparowane z podziałem .impl).
allow_traced_references Umożliwia bezpośrednie kompilowanie do DEX zależnych elementów docelowych (połączonych przez instrumentation_for lub libs, takich jak testy między repozytoriami lub testy dostawców, których nie można wymienić w trace_references_from), aby można było śledzić ich odwołania do kodu bajtowego w tym elemencie docelowym przez ustawienie trace_references: true.
trace_references Jeśli ten parametr ma wartość true w przypadku bezpośredniej zależności kompilacji DEX celu z allow_traced_references: true, uruchamia R8 tracereferences w tym module i przekazuje wygenerowane reguły zachowywania do modułu docelowego.
protect_api_surface Przekazuje --protect-api-surface do R8. Jeśli jest ustawiona na java_sdk_library, automatycznie generuje i dołącza reguły zachowywania interfejsu API Metalava we wszystkich włączonych zakresach interfejsu API (publicznym, systemowym, module-lib, system-server i testowym) do biblioteki implementacji.

Biblioteki statyczne (importy android_library, java_library i AAR) mogą też ustawić required_downstream_optimization: { enabled: true, optimize: true } w Android.bp, aby wymagać, żeby każdy docelowy element kompilacji DEX, który statycznie je łączy (bezpośrednio lub przejściowo), włączał kompresję R8 i optymalizację kodu bajtowego. Soong automatycznie włącza tę funkcję na platformie androidx.compose.runtime_runtime, dzięki czemu cele łączące Jetpack Compose są optymalizowane za pomocą R8.

Włącz zaciemnianie, gdzie tylko jest to możliwe.

W Soong obfuscate domyślnie ma wartość false ze względu na zgodność z poprzednimi wersjami, ale w miarę możliwości należy wyraźnie ustawić obfuscate: true na android_app i samodzielnych obiektach docelowych DEX:

  • Mniejszy rozmiar plików DEX i .vdex: zmiana nazw pakietów, klas, pól i metod na krótkie identyfikatory (a, b) zmniejsza pulę ciągów znaków DEX (string_ids i string_data_item) oraz deskryptory typów. Android mapuje pliki .vdex i .odex na pamięć procesu, więc mniejsze tabele symboli bezpośrednio zmniejszają rozmiar partycji systemowej i wykorzystanie pamięci w czasie działania.
  • Głębsze optymalizacje całego programu: umożliwienie R8 zmiany nazw identyfikatorów odblokowuje scalanie klas, spłaszczanie pakietów i deduplikację klas syntetycznych w pakietach, które w przeciwnym razie R8 musi pominąć, aby uniknąć kolizji nazw.
  • Pełna symbolizacja zrzutu stosu: włączenie zaciemniania kodu w Soong nie zmniejsza możliwości debugowania. W przypadku każdego celu skompilowanego przez R8 Soong generuje plik mapowania proguard_dictionary w katalogu pośrednim modułu, grupuje wszystkie słowniki modułu w artefakt kompilacji proguard-dict.zip, umieszcza skrót mapowania (--map-id-template) w nagłówku DEX i ponownie zapisuje atrybuty klasy SourceFile (--source-file-template), aby retrace mógł automatycznie symbolizować zrzuty stosu.

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

  • Biblioteki udostępnione w ścieżce bootclasspath lub ścieżce klasy system_server: moduły (java_library lub java_sdk_library), które udostępniają zewnętrzną powierzchnię interfejsu API połączoną dynamicznie przez inne moduły w czasie działania (np. cele framework.jar, services.jar lub <uses-library>), muszą zachować obfuscate: false lub wyraźnie zachować powierzchnię interfejsu API za pomocą reguł protect_api_surface: true, @KeepForApi lub -keep, aby elementy wywołujące skompilowane na podstawie ich kodów pośredniczących mogły rozpoznawać nazwy klas i składowych w czasie działania. W modułach java_sdk_library ustawienie protect_api_surface: true automatycznie generuje i dołącza reguły zachowywania Metalava ProGuard we wszystkich włączonych zakresach interfejsu API (publicznym, systemowym, module-lib, system-server i testowym) do biblioteki implementacji. Gdy uogólniona optymalizacja R8 jest włączona w przypadku nieaktualizowalnych elementów docelowych bootclasspath (framework-minus-apex, ext i framework-location), zawierają one również plik framework-hiddenapi-proguard-rules (wygenerowany przez hiddenapi_to_proguard), aby zachować elementy wymienione w aktywnych plikach boot/hiddenapi/*.txt (z pominięciem hiddenapi-max-target-o.txt).

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

  • Zależności zewnętrznego testowego pliku APK: jeśli zewnętrzny plik APK android_test ustawia instrumentation_for: "MyApp" i bezpośrednio wywołuje klasy wewnętrzne lub metody MyApp, zmiana nazwy tych symboli powoduje błąd NoSuchMethodError lub NoClassDefFoundError w czasie wykonywania testu. Skonfiguruj trace_references_from (lub allow_traced_references z trace_references: true), utwórz test jako samodzielnie instrumentujący plik APK łączący statycznie MyApp.impl lub dodaj adnotacje do punktów zaczepienia testu za pomocą @VisibleForTesting (patrz Krok 2. Przenieś reguły zachowywania oparte na testach), aby symbole wewnętrzne potrzebne do testów zostały zachowane podczas zaciemniania reszty aplikacji.
  • Refleksja oparta na ciągach znaków i JNI: jeśli moduł wyszukuje klasy, metody lub pola według nazw ciągów znaków (Class.forName, getDeclaredMethod lub JNI FindClass i GetMethodID) bez reguł zachowywania lub adnotacji, zaciemnianie kodu zmienia nazwy tych elementów docelowych i uniemożliwia wyszukiwanie w czasie działania. (Pamiętaj, że nieoznaczona refleksja nie działa też w 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.

Zasada braku niestandardowych flag

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

  • Pliki bazowe platformy i AAPT2: większość punktów wejścia jest już automatycznie zachowywana przez globalne pliki bazowe platformy (takie jak @Keep, metody JNI native i @VisibleForTesting) lub generowana przez AAPT2 z AndroidManifest.xml i zasobów układu (patrz Omówienie domyślnych reguł zachowywania platformy).
  • Ukierunkowane alternatywy: jeśli punkty wejścia muszą zostać zachowane, preferuj adnotacje w miejscu deklaracji (keepanno, @VisibleForTesting), wyeksportowane reguły biblioteki (export_proguard_flags_files: true) lub statyczne testowe linkowanie 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 bazy lub czy można ją przenieść do adnotacji kodu.

Domyślne reguły przechowywania na platformie

Soong automatycznie przekazuje te globalne reguły podstawowe do każdego wywołania R8 w celach kompilacji DEX (android_app, android_test i moduły java_library z możliwością zainstalowania) skonfigurowanych w build/soong/java/dex.go:

  • build/make/core/proguard.flags: zachowuje klasy i elementy z adnotacją @com.android.internal.annotations.VisibleForTesting we wszystkich pakietach oraz z 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 dla zrzutów stosu, adnotacje widoczności środowiska wykonawczego (RuntimeVisible*Annotations), atrybuty Exceptions i AnnotationDefault, metody native, elementy Serializable, metody @JavascriptInterface, konstruktory Throwable(String), pola Parcelable$CREATOR i pola protobuf MessageLite.
  • build/make/core/proguard/kotlin.flags: wycisza niegroźne ostrzeżenia dotyczące określonych metaanotacji Kotlina (kotlin.Metadata i kotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target}) i usuwa adnotacje Kotlina DebugMetadata w kompilacjach do publikacji.
  • build/make/core/proguard/checknotnull.flags: zastępuje typowe wywołania pomocnicze sprawdzania wartości null (com.google.common.base.Preconditions.checkNotNull i dagger.internal.Preconditions.checkNotNull*) zwięzłymi sprawdzeniami wartości null w kodzie bajtowym. Ten plik celowo pomija Objects.requireNonNull, aby zachować komunikaty o wyjątkach w ramach interfejsu API platformy.
  • build/make/core/proguard/enumvalues.flags: zachowuje metody values i valueOf w typach enum, chyba że zostały wyłączone w konfiguracji kompilacji.
  • Reguły wygenerowane automatycznie przez AAPT2: AAPT2 sprawdza scalone pliki AndroidManifest.xml, pliki XML układu i pliki XML preferencji, aby wygenerować dokładne reguły zachowania dla każdego zarejestrowanego elementu Activity, Service, BroadcastReceiver, ContentProvider, BackupAgent, Application, niestandardowej podklasy View, podklasy Preference i metody XML android:onClick.

Sprawdzanie i migracja istniejących reguł przechowywania

Podczas sprawdzania istniejących plików proguard.flags lub keep.xml w repozytorium platformy oceniaj każdą regułę po kolei według tej 4-stopniowej hierarchii:

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

Usuń reguły, które są już objęte globalnymi punktami odniesienia, regułami układu i pliku manifestu 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ą nadmiarowe, 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 (takie jak 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 zachowywania oparte na testach z niestandardowych plików produkcyjnych .flags, używając jednego z tych wzorców:

  • Śledzenie odwołań z celów testowych lub towarzyszących (trace_references_from lub allow_traced_references): gdy zarówno cel, jak i bezpośrednio zależny moduł kompilują własny plik DEX (android_app, android_test lub java_library) i są połączone za pomocą instrumentation_for lub libs, Soong może uruchomić R8 tracereferences w zależnym module i przekazać wygenerowane reguły zachowania do kompilacji R8 celu bez tworzenia cyklu zależności ani konieczności ręcznego podziału biblioteki .impl:

    • Testy w tym samym repozytorium (trace_references_from): wyświetl listę bezpośrednich zależności instrumentation_for lub libs od modułu docelowego (jest to również automatycznie propagowane w przypadku wariantów override_android_app i override_android_test, a także nadal obsługuje niezależne biblioteki towarzyszące Java, gdy są sparowane z podziałem .impl):

      android_app {
          name: "MyApp",
          srcs: ["src/**/*.java"],
          optimize: {
              obfuscate: true,
              shrink_resources: true,
              trace_references_from: ["MyAppTests"],
          },
      }
      
      android_test {
          name: "MyAppTests",
          instrumentation_for: "MyApp",
          srcs: ["tests/**/*.java"],
      }
      
    • Testy między repozytoriami lub testy dostawcy (allow_traced_references i trace_references): gdy moduł docelowy nie może wyświetlić zależności bezpośrednio w trace_references_from (np. testy w innym repozytorium lub drzewie dostawcy), ustaw allow_traced_references: true w module docelowym i wyraź zgodę z każdej zależności za pomocą trace_references: true:

      android_app {
          name: "MyApp",
          srcs: ["src/**/*.java"],
          optimize: {
              allow_traced_references: true,
          },
      }
      
      android_test {
          name: "MyAppVendorTests",
          instrumentation_for: "MyApp",
          srcs: ["tests/**/*.java"],
          optimize: {
              trace_references: true,
          },
      }
      
    • Refleksja oparta na ciągach znaków w testach: tracereferences sprawdza odwołania do puli stałych kodu bajtowego i nie może wykryć refleksji opartej na ciągach znaków w testach (Class.forName, getDeclaredField, getDeclaredMethod ani pomocników wstrzykiwania przez pola). Dodaj adnotacje do pól i metod produkcyjnych, do których dostęp jest uzyskiwany przez refleksję, za pomocą @VisibleForTesting (lub @UsedByReflection).

  • Połącz bibliotekę implementacji z testami (static_libs): jeśli nie chcesz, aby w produkcyjnym pliku APK były zachowywane punkty wejścia tylko do testów, i nie chcesz, aby zmiany w testach ponownie uruchamiały R8 w celu produkcyjnym, umieść pliki źródłowe aplikacji w android_library (MyApp.impl) i statycznie połącz 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 wzorzec z 3 modułami umożliwia uruchamianie MyAppTests jako testu z instrumentacją z pełnym dostępem do klas wewnętrznych, a jednocześnie pozwala na zmniejszanie i zaciemnianie MyApp wyłącznie w odniesieniu do produkcyjnych punktów wejścia.

  • Oznaczanie punktów zaczepienia testu adnotacją @VisibleForTesting: jeśli zewnętrzny testowy plik APK wywołuje tylko kilka metod wewnętrznych lub konstruktorów w celu android_app, możesz oznaczyć te deklaracje adnotacją @VisibleForTesting. Globalny plan bazowy build/make/core/proguard.flags automatycznie zachowuje @VisibleForTesting elementy w pakietach android.**, com.android.** i com.google.android.** bez niestandardowych plików .flags. (W przypadku modułów dostawcy spoza tych przestrzeni nazw użyj reguł trace_references_from, @UsedByReflection lub reguł wyeksportowanej biblioteki). Jeśli dodajesz adnotacje ręcznie za pomocą @VisibleForTesting zamiast trace_references_from lub trace_references, dodaj też adnotacje do klas przekazywanych do Mockito mock(...) lub spy(...) (nie tylko do metod zastępczych), ponieważ mockito-target (dexmaker-mockito) na urządzeniu generuje podklasy środowiska wykonawczego, a R8 może oznaczać klasy bez podklas jako final (MockitoException: Can't mock/spy class ... - final class).

  • Unikaj nakładających się bibliotek statycznych lub przepakuj je (jarjar_rules): gdy zarówno plik APK android_app, jak i testowy plik APK instrumentation_for statycznie łączą tę samą bibliotekę (bezpośrednio, np. guava, lub przejściowo), tracereferences widzi te klasy zdefiniowane w teście i nie generuje reguł zachowania dla aplikacji, a w czasie działania nadrzędny moduł ładujący klasy aplikacji przesłania kopię testu klasami zmniejszonymi przez R8, co powoduje błąd IllegalAccessError lub NoSuchMethodError. Jeśli test nie potrzebuje własnej kopii pakietu, usuń duplikat z pliku static_libs testu (lub połącz go za pomocą libs), aby uniknąć kolizji. W przeciwnym razie przepakuj nakładające się pakiety bibliotek statycznych aplikacji pod prefiksem modułu za pomocą jarjar_rules.

Krok 3. Wyeksportuj reguły z biblioteki właściciela

Jeśli udostępniony element java_library lub android_library wymaga reguł zachowywania na potrzeby własnej wewnętrznej refleksji lub wywołań zwrotnych JNI, zdefiniuj reguły w docelowej bibliotece 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 elementy podrzędne android_app i biblioteki, które są połączone statycznie z my-shared-library, automatycznie dziedziczą te reguły, więc aplikacje podrzędne nie muszą ich powielać. Gdy 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 odwoływać się do :my-shared-flags w sposób przejrzysty, niezależnie od granic pakietu. Ogólne wskazówki dotyczące tworzenia reguł przechowywania dla konsumentów biblioteki bez ograniczania optymalizacji aplikacji znajdziesz w artykule Optymalizacja dla autorów bibliotek.

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

Gdy dostęp do klasy, metody, konstruktora lub pola jest uzyskiwany przez refleksję lub JNI i nie jest on objęty przez AAPT2 ani globalne bazy, zastąp odłączone reguły -keep w plikach .flags adnotacjami źródłowymi z przewodnika po adnotacjach Keep (zobacz dokumentację Javadoc keepanno):

  • @UsesReflection: używaj tej adnotacji, gdy kontrolujesz kod wykonujący refleksję. Umieść go w miejscu wywołania refleksji, aby zadeklarować, do których klas, metod lub pól docelowych uzyskiwany jest dostęp dynamiczny. @UsesReflection automatycznie koduje warunek wstępny, zgodnie z którym wywołanie z adnotacją musi być osiągalne, dlatego R8 usuwa zarówno wywołującego, jak i cel refleksyjny, jeśli wywołanie nie jest używane.
  • @UsedByReflection: umieść w klasach, metodach, polach lub konstruktorach, które są tworzone lub wywoływane przez zewnętrzny kod lub biblioteki, takie jak klasy wczytywane za pomocą Class.forName z kluczy Bundle lub Settings, albo klasy wtyczek wczytywane przez dynamiczne moduły ładujące klasy. Określ kind (np. KeepItemKind.CLASS_AND_METHODS), preconditions (@KeepCondition) i ograniczenia parametrów, aby R8 zachowywał tylko dokładną umowę dotyczącą refleksji i mógł nadal optymalizować lub usuwać nieużywane elementy klasy. Nie dodawaj @UsedByReflection do komponentów zarejestrowanych w pliku manifestu, takich jak JobService lub BroadcastReceiver, ponieważ AAPT2 już je przechowuje.
  • @UsedByNative: umieść w metodach lub polach, do których dostęp jest uzyskiwany z kodu JNI 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żywaj jako opcji zastępczej, gdy nie można zastosować keepanno. Zastosowanie reguły @Keep do klasy powoduje bezwarunkowe zachowanie klasy i wszystkich jej elementów. Zastosowanie adnotacji @Keep do metody lub pola działa jak bezwarunkowy punkt wejścia, który zachowuje element i zawierającą go klasę, nawet jeśli klasa nigdy nie zostanie utworzona, podczas gdy adnotacja keepanno wyraża warunkową osiągalność.

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 wywołania w kodzie źródłowym Javy lub Kotlina:

    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ńcowego wyniku DEX, nie dodając żadnego narzutu na kod bajtowy w czasie działania.

Unikanie typowych pułapek

W sekcjach poniżej opisujemy częste błędy proguard.flags i keep.xml w kodzie platformy oraz sposoby ich naprawiania.

Reguły zachowywania komponentów przybliżonych

# 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 wydajność: ta reguła jest całkowicie zbędna w przypadku AAPT2. Ponieważ używa symbolu wieloznacznego bez sprawdzania, czy komponent jest zarejestrowany w ostatecznym scalonym pliku AndroidManifest.xml, wymusza na R8 zachowanie każdej podklasy Activity, Service lub BroadcastReceiver znalezionej w dowolnym miejscu w ścieżce klasy, w tym nieużywanych komponentów biblioteki i wyłączonych aktywności debugowania.
  • Zalecane rozwiązanie: usuń regułę. AAPT2 sprawdza manifest łączony i generuje dokładne reguły -keep dla zarejestrowanych komponentów.

Reguły zachowywania modułu obsługi kliknięć w szerokim formacie XML

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

Reguły zachowywania metod pobierających i ustawiających widoku Broad View

# 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 wpływa to negatywnie na wydajność: ta reguła wymusza na R8 zachowanie wszystkich metod pobierających i ustawiających w każdej podklasie View w aplikacji i wszystkich połączonych bibliotekach (w tym bibliotekach AndroidX i Material), co uniemożliwia usuwanie nieużywanych metod i wstawianie ich w kodzie interfejsu.
  • Zalecane rozwiązanie: usuń regułę. AAPT2 zachowuje już konstruktory niestandardowych klas View wyodrębnionych z plików XML układu. Jeśli kod animuje właściwość widoku za pomocą nazw ciągów znaków, takich jak ObjectAnimator.ofFloat(view, "translationZ", ...), zastąp nazwę ciągu znaków odwołaniem do właściwości (View.TRANSLATION_Z lub niestandardową implementacją FloatProperty lub IntProperty), aby całkowicie uniknąć refleksji. Jeśli nie można uniknąć dostępu do właściwości refleksyjnej, dodaj do konkretnej metody pobierającej lub ustawiającej adnotację @UsedByReflection.

Wyłączenie ogólnej optymalizacji lub zaciemniania kodu

# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
  • Dlaczego wpływa to negatywnie na wydajność: umieszczenie -dontoptimize lub -dontshrink w pliku .flags powoduje niezauważalne zastąpienie ustawień Android.bp modułu i wyłączenie optymalizacji w całym środowisku docelowym. Co gorsza, jeśli biblioteka eksportuje plik .flags zawierający -dontoptimize, wyłącza optymalizację R8 dla każdego pliku android_app, który łączy się z biblioteką.
  • Zalecane rozwiązanie: usuń -dontoptimize, -dontshrink i -dontobfuscate z .flags plików. Kontroluj zachowanie optymalizacji bezpośrednio w Android.bp, używając bloku optimize (shrink, optimize, obfuscate) w docelowym elemencie liścia.

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ą logi kompilacji lub próbują zapisywać pliki wyjściowe w ścieżkach lokalnych podczas kompilacji Soong w 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 out/soong/.intermediates/.

Reguły zachowywania kodu testowego w wersji produkcyjnej

  • Dlaczego wpływa to negatywnie na wydajność: dodanie niestandardowych reguł -keep wyłącznie na potrzeby dostępu testowego powoduje, że kod pozostaje niezaciemniony i jest zachowywany w kompilacjach produkcyjnych. Podczas konfigurowania trace_references_from (lub allow_traced_references z trace_references: true) lub dodawania adnotacji do metod za pomocą @VisibleForTesting unika się ręcznego utrzymywania reguł -keep i zachowuje tylko to, do czego odwołują się testy, ale te symbole nadal są dostarczane w pliku binarnym produkcyjnym.
  • Zalecane rozwiązanie: wyeliminuj niestandardowe reguły testowe -keep, włączając trace_references_from (lub allow_traced_references i trace_references: true), albo podziel aplikację na bibliotekę .impl połączoną z samoinstrumentującym testem z instrumentacją w pliku APK za pomocą static_libs, aby punkty wejścia tylko do testów były całkowicie poza produkcyjnym plikiem APK, zgodnie z opisem w sekcji Krok 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 zachowują wszystkie zasoby pasującego typu w module i jego zależnościach, co uniemożliwia zmniejszanie zasobów (shrink_resources: true) i powoduje zwiększenie rozmiaru pliku APK oraz tabeli mapowania resources.arsc.
  • Zalecane rozwiązanie:
    • Używaj odwołań do zasobów statycznych zamiast keep.xml: unikaj używania metody Resources.getIdentifier do dynamicznego wyszukiwania ograniczonego zbioru zasobów, takich jak numerowane ciągi eksperymentalne lub elementy graficzne o określonej tematyce. Zamiast tego użyj instrukcji switch w czasie kompilacji lub mapuj na stałe statyczne R.id, R.string lub R.drawable. Statyczne odwołania umożliwiają R8 i AAPT2 śledzenie dokładnych zasobów aktywnych, eliminują narzut związany z wyszukiwaniem ciągów znaków w czasie działania i całkowicie eliminują potrzebę stosowania keep.xml.
    • Podaj nazwy konkretnych zasobów: jeśli wymagane jest dynamiczne wyszukiwanie, np. przez czytnik licencji innej firmy, podaj dokładne identyfikatory zasobów w polu 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 zachowuje je automatycznie, więc możesz usunąć plik res/raw/keep.xml.

Weryfikowanie i audytowanie zmian reguł

Za każdym razem, gdy usuwasz lub zawężasz reguły zachowywania w pliku proguard.flags lub keep.xml, sprawdź, czy moduł kompiluje się bez błędów, przechodzi testy jednostkowe i testy z instrumentacją oraz zachowuje wszystkie wymagane punkty wejścia.

Tworzenie i testowanie modułów

  1. Skompiluj moduł, aby sprawdzić, czy R8 i AAPT2 kończą działanie bez ostrzeżeń o braku odniesień:

    m <MODULE_NAME>
    
  2. Uruchom testy jednostkowe i testy z instrumentacją dla modułu, używając polecenia atest:

    atest <TEST_MODULE_NAME>
    
  3. W przypadku aplikacji systemowych, usług uprzywilejowanych lub modułów zależnych od sprzętu uruchom testy instrumentacji i interfejsu na docelowych urządzeniach fizycznych lub w laboratorium testowym urządzeń, aby sprawdzić ścieżki refleksji 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 zmianami reguł i po nich, aby sprawdzić, czy R8 usuwa nieużywany 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
    
  • Podczas uruchamiania prebuilts/r8/r8.jar bezpośrednio na module classes.jar przed R8 w celu sprawdzenia zmian flag offline bez pełnej kompilacji platformy przekaż zarówno pliki JAR ścieżki klasy --lib modułu (android.jar, framework.jar i udostępniony libs), jak i wygenerowane przez AAPT2 reguły proguard.options z AndroidManifest.xml i plików XML układu. Bez tych danych wejściowych R8 nie może rozpoznać nadtypów platformy (Activity, Service, Phone, Handler) i nie zachowuje metod hierarchii.

Analizowanie reguł zachowywania i subsumpcji reguł za pomocą R8

Kompilator R8 typu open source 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 Androida (skonfigurowaną w pliku build/soong/java/dex.go).

Aby przeanalizować reguły zachowywania dla 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 zmienna R8_DUMP_KEEP_RADIUS=true, Soong instruuje R8, aby rejestrował dane reguł zachowywania w pośrednim pliku r8keepradius.pb dla każdego skompilowanego modułu w out/soong/.intermediates/. W przypadku każdej reguły zachowywania i adnotacji keepanno R8 rejestruje:

    • Bezpośredni zakres zachowania: dokładne klasy, pola i metody zachowywane przez regułę wraz z określonymi ograniczeniami, które wymusza ona w odniesieniu do zmniejszania, optymalizacji lub zaciemniania.
    • Zastępowanie reguł: które inne reguły przechowywania lub adnotacje już przechowują te same elementy. Jeśli reguła niestandardowa jest całkowicie objęta regułą AAPT2 lub globalną regułą podstawową, możesz ją bezpiecznie usunąć.
    • Reguły dotyczące całego pakietu i reguły globalne: reguły, które używają symboli wieloznacznych dotyczących całego pakietu lub stosują globalne dyrektywy konfiguracji.
  2. Przekonwertuj dane wyjściowe r8keepradius.pb na interaktywny raport HTML za pomocą KeepRadiusHtmlReportGenerator (w pakiecie z 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 katalog, KeepRadiusHtmlReportGenerator przechodzi przez wszystkie pliki *keepradius*.pb, generuje raport HTML dla każdego modułu i tworzy plik out/keep_radius_reports/keepradius.html podsumowujący liczbę aktywnych elementów, liczbę zachowanych elementów i reguły zachowywania o największym promieniu w całej kompilacji. Więcej informacji o interpretowaniu wyników zmniejszania, optymalizacji i zaciemniania kodu w wygenerowanym raporcie znajdziesz w artykule Korzystanie z analizatora konfiguracji R8.

Soong obsługuje też dodatkowe zmienne środowiskowe diagnostyki R8 i pętli wewnętrznej w pliku 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 scalone konfiguracje ProGuard na potrzeby samodzielnego odtworzenia 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.
  • SOONG_SKIP_TRACE_REFERENCES=true: pomija uruchamianie reguł tracereferences skonfigurowanych przez trace_references_from lub trace_references, aby przyspieszyć lokalne kompilacje pętli wewnętrznej podczas iteracji kodu testowego.

Sprawdzanie reguł dotyczących klientów biblioteki

Gdy java_library lub android_library eksportuje reguły zachowywania do konsumentów na dalszych etapach (export_proguard_flags_files: true), reguły te nie mogą zawierać flag globalnych, które zmieniają lub wyłączają optymalizację aplikacji użytkownika.

R8 udostępnia analizator reguł zachowywania typu open source (ProcessKeepRules), który jest dostępny w kompilacji platformy Androida 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 konfiguracji i w przypadku napotkania dyrektyw niedozwolonych w regułach konsumenta biblioteki kończy działanie, podając informacje diagnostyczne o pliku i wierszu. Niedozwolone dyrektywy obejmują wyłączenie globalnej optymalizacji, zmniejszania lub zaciemniania (np. -dontoptimize lub -dontshrink), flagi ponownego pakowania i modyfikacji dostępu, flagi diagnostyczne lub mapowania oraz -keepattributes na poziomie aplikacji.

Kontrola drzew źródłowych za pomocą pgaudit.py

Aby przeprowadzić kontrolę nieskompilowanych katalogów źródłowych wraz z analizatorami czasu kompilacji R8, drzewo platformy Androida zawiera skrypt pgaudit.py w build/make/core/proguard/tools/. To narzędzie do analizy statycznej skanuje pliki Android.bp, proguard.flags i keep.xml w repozytorium, aby oznaczyć reguły, które są już objęte przez AAPT2 lub globalne bazy platformy, szerokie symbole wieloznaczne, zastąpienia -dontoptimize i reguły zachowywania tylko do 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 ds. platformy i OEM mogą połączyć pgaudit.py, aby szybko przeprowadzić selekcję drzewa źródłowego, process-keep-rules, aby zweryfikować wyeksportowane reguły biblioteki, oraz R8_DUMP_KEEP_RADIUS=true z KeepRadiusHtmlReportGenerator, aby zmierzyć dokładny promień przechowywania klas, pól i metod każdej reguły w kompilacji.