Optymalizacja dla autorów bibliotek

Jako autor biblioteki musisz zadbać o to, aby deweloperzy aplikacji mogli łatwo zintegrować Twoją bibliotekę z aplikacją, zachowując przy tym wysoką jakość obsługi użytkowników. Oznacza to, że Twoja biblioteka musi być zgodna z optymalizacją Androida (R8) bez konieczności dodatkowej konfiguracji przez dewelopera. Jeśli biblioteka może być nieodpowiednia do użycia na Androidzie, musisz to udokumentować. Kluczowe jest, aby biblioteki przeznaczone do użycia na Androidzie nie uniemożliwiały ważnych optymalizacji aplikacji i były zgodne z dodatkowymi wymaganiami dotyczącymi optymalizacji.

Ta dokumentacja jest przeznaczona dla deweloperów opublikowanych bibliotek, ale może być też przydatna dla deweloperów wewnętrznych modułów bibliotek w dużej, modułowej aplikacji.

Jeśli jesteś deweloperem aplikacji i chcesz dowiedzieć się, jak zoptymalizować aplikację na Androida, przeczytaj artykuł Włączanie optymalizacji aplikacji. Aby dowiedzieć się, które biblioteki są odpowiednie do użycia, przeczytaj artykuł Wybieranie bibliotek.

Typy reguł zachowywania

W bibliotekach możesz mieć 2 różne typy reguł zachowywania:

  • Reguły zachowywania konsumenta muszą określać reguły, które zachowują wszystko, co odzwierciedla biblioteka. Jeśli biblioteka używa odbicia lub JNI do wywoływania swojego kodu lub kodu zdefiniowanego przez aplikację klienta, te reguły muszą opisywać, który kod należy zachować. Biblioteki powinny zawierać reguły zachowywania konsumenta, które mają taki sam format jak reguły zachowywania aplikacji. Te reguły są dołączane do artefaktów biblioteki (AAR lub JAR) i są automatycznie używane podczas optymalizacji aplikacji na Androida, gdy biblioteka jest używana. Te reguły są przechowywane w pliku określonym przez właściwość consumerProguardFiles w build.gradle.kts (lub build.gradle) pliku. Więcej informacji znajdziesz w artykule Pisanie reguł zachowywania konsumenta.
  • Reguły zachowywania kompilacji biblioteki są stosowane podczas kompilowania biblioteki. Są one potrzebne tylko wtedy, gdy zdecydujesz się częściowo zoptymalizować bibliotekę w czasie kompilacji. Muszą one uniemożliwiać usunięcie publicznego interfejsu API biblioteki, w przeciwnym razie nie będzie on dostępny w dystrybucji biblioteki, co oznacza, że deweloperzy aplikacji nie będą mogli używać biblioteki. Te reguły są przechowywane w pliku określonym przez właściwość proguardFiles w pliku build.gradle.kts (lub build.gradle). Więcej informacji znajdziesz w artykule Optymalizowanie kompilacji biblioteki AAR.

Wymagania i wytyczne dotyczące optymalizacji

Konfiguracja R8 w bibliotekach ma globalny wpływ na ostateczny rozmiar binarny i wydajność aplikacji. Oprócz ogólnych sprawdzonych metod dotyczących reguł zachowywaniaautorzy bibliotek muszą przestrzegać określonych wymagań i stosować się do dodatkowych wytycznych.

Przestrzeganie wymagań dotyczących optymalizacji

Niewydajność bibliotek jest główną przyczyną rozrostu aplikacji, marnowania pamięci, powolnego uruchamiania i błędów Aplikacja nie odpowiada (ANR). Biblioteki muszą unikać naruszania tych wymagań, aby nie obniżać znacząco jakości aplikacji i wygody użytkowników.

  • Brak ogólnych reguł zachowywania lub reguł zachowywania całego pakietu: Twoja biblioteka nie może zawierać ogólnych reguł zachowywania, które zachowują większość kodu w bibliotece lub w innej bibliotece. Ogólne reguły zachowywania mogą rozwiązać problemy z awariami w krótkim okresie, ale zwiększają rozmiar aplikacji wszystkich aplikacji, które korzystają z Twojej biblioteki.

    Nie umieszczaj reguł zachowywania całego pakietu (np. -keep class com.mylibrary.** {*; }) w przypadku pakietów w bibliotece lub innych bibliotekach, do których się odwołujesz. Takie reguły ograniczają optymalizację tych pakietów we wszystkich aplikacjach, które korzystają z Twojej biblioteki.

  • Brak nieodpowiednich reguł globalnych: nigdy nie używaj opcji globalnych, takich jak -dontobfuscate czy -allowaccessmodification.

  • W miarę możliwości używaj generowania kodu zamiast odbicia: jeśli to możliwe, używaj generowania kodu (codegen) zamiast odbicia. Generowanie kodu i odbicie to powszechne metody unikania powtarzalnego kodu podczas programowania, ale generowanie kodu jest bardziej zgodne z optymalizatorem aplikacji, takim jak R8.

    W przypadku generowania kodu kod jest analizowany i modyfikowany podczas procesu kompilacji. Ponieważ po kompilacji nie ma większych modyfikacji, optymalizator wie, który kod jest ostatecznie potrzebny, a który można bezpiecznie usunąć.

    W przypadku odbicia kod jest analizowany i manipulowany w czasie działania. Ponieważ kod nie jest ostatecznie gotowy do momentu wykonania, optymalizator nie wie, który kod można bezpiecznie usunąć. Prawdopodobnie usunie kod, który jest używany dynamicznie przez odbicie w czasie działania, co powoduje awarie aplikacji u użytkowników.

    Wiele nowoczesnych bibliotek używa generowania kodu zamiast odbicia. Więcej informacji znajdziesz w artykule KSP – typowy punkt wejścia używany przez Room, Hilt i wiele innych bibliotek.

  • Obsługa pełnego trybu R8: Twoja biblioteka nie powinna się zawieszać, gdy włączony jest pełny tryb R8. Pełny tryb R8 to zalecany tryb używania R8. Jest on domyślnie włączony od wersji AGP 8.0, która została ustabilizowana w 2023 r. Jeśli Twoja biblioteka zawiesza się w R8, rozwiązaniem jest zidentyfikowanie konkretnego punktu wejścia odbicia lub JNI i dodanie reguły docelowej, a nie zachowywanie całego pakietu.

Dodatkowe zalecenia

Oprócz wymagań dotyczących optymalizacji obowiązują też te dodatkowe zalecenia.

  • Nie używaj -repackageclasses w pliku reguł zachowywania konsumenta biblioteki. Aby jednak zoptymalizować kompilację biblioteki, możesz użyć -repackageclasses z wewnętrzną nazwą pakietu, np. <your.library.package>.internal, w pliku reguł zachowywania kompilacji biblioteki. Może to zwiększyć wydajność biblioteki w niezoptymalizowanych aplikacjach. Nie jest to jednak zwykle konieczne, ponieważ aplikacje też powinny być zoptymalizowane.
  • Zadeklaruj wszystkie atrybuty, których potrzebujesz do działania biblioteki, w plikach reguł zachowywania biblioteki, nawet jeśli mogą się one pokrywać z atrybutami zdefiniowanymi w proguard-android-optimize.txt.
  • Jeśli w dystrybucji biblioteki potrzebujesz tych atrybutów, zachowaj je w pliku reguł zachowywania kompilacji biblioteki, a nie w pliku reguł zachowywania konsumenta biblioteki:
    • AnnotationDefault
    • EnclosingMethod
    • Exceptions
    • InnerClasses
    • RuntimeInvisibleAnnotations
    • RuntimeInvisibleParameterAnnotations
    • RuntimeInvisibleTypeAnnotations
    • RuntimeVisibleAnnotations
    • RuntimeVisibleParameterAnnotations
    • RuntimeVisibleTypeAnnotations
    • Signature
  • Jeśli adnotacje są używane w czasie działania, autorzy bibliotek powinni zachować atrybut RuntimeVisibleAnnotations w regułach zachowywania konsumenta.
  • Autorzy bibliotek nie powinni używać tych opcji globalnych w regułach zachowywania konsumenta:
    • -include
    • -basedirectory
    • -injars
    • -outjars
    • -libraryjars
    • -repackageclasses
    • -flattenpackagehierarchy
    • -allowaccessmodification
    • -renamesourcefileattribute
    • -ignorewarnings
    • -addconfigurationdebugging
    • -printconfiguration
    • -printmapping
    • -printusage
    • -printseeds
    • -applymapping
    • -obfuscationdictionary
    • -classobfuscationdictionary
    • -packageobfuscationdictionary

Kiedy można używać odbicia

Jeśli musisz używać odbicia, powinieneś/powinnaś używać go tylko w przypadku:

  • konkretnych typów docelowych (konkretnych implementacji interfejsów lub podklas);
  • kodu używającego konkretnej adnotacji w czasie działania.

Używanie odbicia w ten sposób ogranicza koszty w czasie działania i umożliwia pisanie docelowych reguł zachowywania konsumenta.

Ta konkretna i docelowa forma odbicia to wzorzec, który można zaobserwować w platformie Androida (np. podczas rozszerzania aktywności, Widoków aktywnych i obiektów rysowalnych) i w bibliotekach AndroidX (np. podczas tworzenia WorkManager ListenableWorkers lub RoomDatabases). Natomiast otwarte odbicie Gson nie nadaje się do użycia w aplikacjach na Androida.

Często występujące nieporozumienia

Kilka często występujących nieporozumień może prowadzić do nieprawidłowej konfiguracji R8. Oto niektóre z nich:

  • Nieprawidłowe zrozumienie optymalizacji R8: wbrew powszechnemu przekonaniu optymalizacje R8 nie ograniczają się tylko do zaciemniania kodu, ale obejmują też zmniejszanie kodu i optymalizacje logiczne za pomocą technik wstawiania metod i łączenia klas. Więcej informacji znajdziesz w artykule Omówienie optymalizacji R8.

  • Pomijanie optymalizacji zaciemnionych bibliotek: częstym błędem jest pomijanie optymalizacji biblioteki, ponieważ została ona zoptymalizowana lub zaciemniona podczas kompilowania do formatu AAR (Android Archive) lub JAR (Java Archive). Optymalizacje podczas kompilacji biblioteki są ograniczone, a Twoja aplikacja nie powinna wyłączać optymalizacji biblioteki przez uwzględnienie jej w regule zachowywania. Więcej informacji znajdziesz w artykule Optymalizowanie kompilacji biblioteki AAR.

  • Nieprawidłowe zrozumienie opcji -keep Reguła -keep uniemożliwia R8 uruchamianie dowolnych etapów optymalizacji. Więcej informacji znajdziesz w artykule Wybieranie odpowiedniej opcji zachowywania.

Sprawdzanie jakości reguł zachowywania konsumenta

Aby sprawdzić, czy Twoja biblioteka nie uniemożliwia zbyt dużej optymalizacji kodu po osadzeniu w aplikacji, użyj przykładowej biblioteki lub aplikacji do testów integracyjnych. Włącz R8 i sprawdź ogólną jakość konfiguracji R8 aplikacji za pomocą analizatora konfiguracji R8.

Jeśli okaże się, że reguły zachowywania konsumenta ograniczają optymalizację dużych części kodu biblioteki, oznacza to, że wymagają one dopracowania. Zawęź reguły zachowywania konsumenta i przetestuj wszystkie aspekty biblioteki, których to dotyczy, w aplikacji z włączonym R8.

Konfigurowanie pakowania reguł

Aby reguły zachowywania konsumenta były stosowane prawidłowo, musisz je odpowiednio spakować w zależności od formatu biblioteki.

Biblioteki AAR

Aby dodać reguły zachowywania konsumenta do biblioteki AAR, użyj opcji consumerProguardFiles w skrypcie kompilacji modułu biblioteki Androida. Więcej informacji znajdziesz w naszych wskazówkach dotyczących tworzenia modułów bibliotek.

Kotlin

android {
    defaultConfig {
        consumerProguardFiles("consumer-proguard-rules.pro")
    }
    ...
}

Dynamiczny

android {
    defaultConfig {
        consumerProguardFiles 'consumer-proguard-rules.pro'
    }
    ...
}

Biblioteki JAR

Aby dołączyć reguły do biblioteki Kotlin lub Java, która jest dostarczana jako plik JAR, umieść plik reguł w katalogu META-INF/proguard/ w końcowym pliku JAR, pod dowolną nazwą. Jeśli na przykład kod znajduje się w katalogu <libraryroot>/src/main/kotlin, umieść plik reguł zachowywania konsumenta w katalogu <libraryroot>/src/main/resources/META-INF/proguard/consumer-proguard-rules.pro a reguły zostaną dołączone w odpowiednim miejscu w wyjściowym pliku JAR.

Sprawdź, czy końcowy plik JAR prawidłowo zawiera reguły, sprawdzając, czy znajdują się one w katalogu META-INF/proguard.

Optymalizowanie kompilacji biblioteki AAR (zaawansowane)

Zwykle nie trzeba bezpośrednio optymalizować kompilacji biblioteki, ponieważ możliwe optymalizacje w czasie kompilacji biblioteki są bardzo ograniczone. Zanim zoptymalizujesz bibliotekę, jako deweloper musisz zastanowić się nad kilkoma etapami optymalizacji i zachowania, zarówno w czasie kompilacji biblioteki, jak i aplikacji.

Jeśli nadal chcesz zoptymalizować bibliotekę w czasie kompilacji, wtyczka Androida do obsługi Gradle to umożliwia.

Kotlin

android {
    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
        configureEach {
            consumerProguardFiles("consumer-rules.pro")
        }
    }
}

Dynamiczny

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                'proguard-rules.pro'
        }
        configureEach {
            consumerProguardFiles "consumer-rules.pro"
        }
    }
}

Pamiętaj, że działanie proguardFiles bardzo różni się od działania consumerProguardFiles:

  • proguardFiles są używane w czasie kompilacji, często razem z getDefaultProguardFile("proguard-android-optimize.txt"), aby określić, która część biblioteki powinna zostać zachowana podczas kompilacji biblioteki. Co najmniej musi to być publiczny interfejs API.
  • consumerProguardFiles są natomiast dołączane do biblioteki, aby wpływać na optymalizacje, które będą wykonywane później, podczas kompilacji aplikacji korzystającej z Twojej biblioteki.

Jeśli na przykład Twoja biblioteka używa odbicia do tworzenia klas wewnętrznych, może być konieczne zdefiniowanie reguł zachowywania zarówno w proguardFiles, jak i consumerProguardFiles.

Jeśli używasz -repackageclasses w kompilacji biblioteki, zmień nazwę pakietu na podpakiet wewnątrz pakietu biblioteki. Na przykład użyj -repackageclasses 'com.example.mylibrary.internal' zamiast -repackageclasses 'internal'.

Obsługa różnych wersji R8 (zaawansowane)

Możesz dostosować reguły do konkretnych wersji R8. Dzięki temu Twoja biblioteka będzie optymalnie działać w projektach, które używają nowszych wersji R8, a dotychczasowe reguły będą nadal używane w projektach ze starszymi wersjami R8.

Aby określić docelowe reguły R8, musisz je uwzględnić w katalogu META-INF/com.android.tools w pliku classes.jar biblioteki AAR lub w katalogu META-INF/com.android.tools biblioteki JAR.

In an AAR library:
    proguard.txt (legacy location, the file name must be "proguard.txt")
    classes.jar
    └── META-INF
        └── com.android.tools (location of targeted R8 rules)
            ├── r8-from-<X>-upto-<Y>/<R8-rule-files>
            └── ... (more directories with the same name format)

In a JAR library:
    META-INF
    ├── proguard/<ProGuard-rule-files> (legacy location)
    └── com.android.tools (location of targeted R8 rules)
        ├── r8-from-<X>-upto-<Y>/<R8-rule-files>
        └── ... (more directories with the same name format)

W katalogu META-INF/com.android.tools może znajdować się kilka podkatalogów o nazwach w formacie r8-from-<X>-upto-<Y> aby wskazać dla których wersji R8 zostały napisane reguły. Każdy podkatalog może zawierać co najmniej 1 plik z regułami R8, o dowolnych nazwach i rozszerzeniach.

Pamiętaj, że części -from-<X> i -upto-<Y> są opcjonalne, wersja <Y> jest wyłączna, a zakresy wersji są zwykle ciągłe, ale mogą się też nakładać.

Na przykład r8, r8-upto-8.0.0, r8-from-8.0.0-upto-8.2.0 i r8-from-8.2.0 to nazwy katalogów reprezentujące zestaw docelowych reguł R8. Reguły w katalogu r8 mogą być używane przez wszystkie wersje R8. Reguły w katalogu r8-from-8.0.0-upto-8.2.0 mogą być używane przez R8 w wersji od 8.0.0 do 8.2.0, ale bez wersji 8.2.0.

Wtyczka Androida do obsługi Gradle używa tych informacji do wybierania wszystkich reguł, które mogą być używane przez bieżącą wersję R8. Jeśli biblioteka nie określa docelowych reguł R8, wtyczka Androida do obsługi Gradle wybierze reguły z dotychczasowych lokalizacji (proguard.txt w przypadku biblioteki AAR lub META-INF/proguard/<ProGuard-rule-files> w przypadku biblioteki JAR).