Narzędzia i skuteczność

Reguły zachowywania zabezpieczeń wpisywania i optymalizacja DEX

Głośniki

Informacje

Informacje o wystąpieniu

Aplikacje na Androida często są niedostatecznie zoptymalizowane z powodu zbyt konserwatywnych reguł zachowywania, często pochodzących z bibliotek innych firm. Prowadzi to do nadmiernego rozmiaru plików APK, wolniejszego uruchamiania „na zimno” i gorszej wydajności w czasie działania.

Dzięki nowym progom wydajności Google Play dotyczącym optymalizacji DEX widoczność aplikacji zależy teraz bezpośrednio od tego, czy wyprzedzasz krzywą optymalizacji.

W tej prezentacji przedstawiamy nowoczesny przepływ pracy R8, który pozwala rozwiązać problem nadmiernej liczby reguł. Nauczysz się:

  • Użyj analizatora konfiguracji R8 i umiejętności, aby zidentyfikować obciążenie związane ze starszymi regułami.
  • Przejście od ogólnych reguł zachowywania z użyciem symboli wieloznacznych do precyzyjnych, bezpiecznych pod względem typu, wygenerowanych adnotacji.
  • Zapobiegaj regresjom optymalizacji dzięki nowym narzędziom interfejsu wiersza poleceń Androida.

Podsumowanie

  • Google Play egzekwuje progi jakości aplikacji, które wymagają optymalizacji kodu na poziomie powyżej 25%. Spełnienie tych wymagań zmniejsza wykorzystanie pamięci, a jednocześnie zwiększa szybkość uruchamiania i częstotliwość błędów ANR.
  • Analizator konfiguracji R8 generuje raport HTML, który pokazuje, które reguły zachowywania zachowują kod. Wyróżnia reguły podrzędne i nieużywane oraz mierzy potencjał optymalizacji przed kompilacją.
  • Zbyt ogólne reguły zachowywania, często wprowadzane w celu zapobiegania awariom refleksji lub odziedziczone z bibliotek, utrudniają optymalizację aplikacji. Jedna reguła z symbolem wieloznacznym obejmująca cały pakiet może zachować dziesiątki nieużywanych klas.
  • Jetpack Keep używa linta do wykrywania refleksji i generowania precyzyjnych adnotacji oraz warunkowych reguł zachowywania bezpośrednio z kodu. Eksportuje standardowe reguły konsumenta zgodne ze starszymi wersjami R8.
  • Interfejs wiersza poleceń Androida sprawdza pakiety aplikacji i pliki AAR, używając dokładnie tej samej logiki co Google Play, bez konieczności przesyłania. Oznacza problematyczne reguły i eksportuje raporty JSON na potrzeby automatycznych testów regresji.

Zasoby

Gemini Notebook

Tematy

  • dex-optimization
  • r8
  • keep-rules
  • jetpack-keep
  • android-cli
  • wydajność-aplikacji

Transkrypcja

Pobierz transkrypcję

Wprowadzenie do optymalizacji DEX

  • Cześć,

  • Nazywam się Breana i pracuję w zespole Android DevRel.

  • A ja jestem Chris Craik i pracuję w zespole ds. wydajności Androida.

  • Witamy w części poświęconej regułom zachowywania z zabezpieczeniami wpisywania i optymalizacji DEX.

  • Dzisiaj porozmawiamy o tym, czym jest optymalizacja DEX, dlaczego warto się nią zainteresować, a także o kilku ciekawych, niedawnych aktualizacjach.

  • Przez długi czas optymalizacja plików DEX była traktowana jako coś, co warto mieć, ale nie jest to konieczne.

  • Możesz skonfigurować je raz i zostawić albo zająć się tym, gdy będziesz mieć więcej czasu.

  • Branża mobilna boryka się jednak ze znacznymi ograniczeniami w zakresie dostaw sprzętu, które wpływają na dostępność pamięci urządzeń. Dlatego dobrze zoptymalizowany plik DEX jest niezbędny, aby aplikacja zużywała mniej pamięci, szybciej się uruchamiała, rzadziej powodowała błędy ANR oraz lepiej renderowała grafikę i działała wydajniej.

  • Aby utrzymać zdrowy ekosystem Androida i zapewnić, że aplikacje będą nadal oferować najwyższą jakość, Google Play ogłosiło nowe wymagania dotyczące jakości aplikacji z progami wydajności, które zaczną obowiązywać w lutym.

  • Obejmuje to progi optymalizacji kodu DEX.

  • Aplikacje muszą osiągnąć wynik powyżej 25% w przypadku zmniejszania rozmiaru, optymalizacji i zaciemniania kodu.

  • Co to oznacza dla Ciebie jako dewelopera aplikacji na Androida?

  • Niezoptymalizowany kod DEX nie tylko negatywnie wpływa na wrażenia użytkowników, ale może też mieć konsekwencje dla widoczności aplikacji i możliwości jej publikowania w Sklepie Play.

  • Jesteś we właściwym miejscu. Po tej prezentacji będziesz mieć narzędzia, które pozwolą Ci nie tylko spełnić te wymagania, ale nawet je przekroczyć.

  • Optymalizację kodu DEX można przeprowadzić za pomocą R8, optymalizatora aplikacji na Androida.

  • Chociaż R8 nie jest ściśle wymagany, jeśli inny optymalizator aplikacji może przekroczyć te progi, jest to zalecana ścieżka, która jest wbudowana bezpośrednio w łańcuch narzędzi Androida.

  • Dobrze zoptymalizowany pakiet aplikacji DEX zużywa mniej pamięci, ale główne korzyści są związane z wydajnością: mniej błędów ANR, szybsze uruchamianie i lepsza wydajność w czasie działania.

  • Optymalizacja aplikacji przynosi realne, sprawdzone i pozytywne rezultaty zarówno dla aplikacji, jak i dla użytkowników.

  • Na przykład dzięki zastosowaniu pełnego trybu R8 i ulepszeniu reguł zachowywania firma Grab była w stanie uzyskać 78% optymalizacji, 79% zmniejszenia i 82% zaciemnienia kodu aplikacji.

  • Patrząc na ten slajd, możesz pomyśleć: „Te liczby są dość wysokie, ale co to właściwie oznacza?”.

  • Dzięki tym ulepszeniom i profilom podstawowym udało się zmniejszyć liczbę błędów ANR o 32% i skrócić czas uruchamiania o 35%.

  • Gdy najpopularniejsze aplikacje po raz pierwszy wdrażają R8, odnotowują średni spadek rozmiaru DEX o 38% i spadek liczby błędów ANR widocznych dla użytkowników o 16%.

  • Jeśli już używasz R8, mamy dla Ciebie świetną wiadomość.

  • W przypadku aplikacji, które już włączyły R8, a następnie podjęły kroki w celu dalszej poprawy optymalizacji, zmniejszania i zaciemniania kodu do poziomu powyżej 50%, odnotowujemy średnio dodatkowy spadek rozmiaru DEX o 25%, skrócenie czasu uruchamiania o 4% i dodatkowy spadek liczby błędów ANR widocznych dla użytkowników o 17%, co stanowi uzupełnienie 16% pokazanych na poprzednim slajdzie.

  • Dziś chcemy pokazać, jak można osiągnąć takie wyniki.

  • Omówimy narzędzia, które pomogą Ci zoptymalizować kod DEX i przejść od zbyt ogólnych i nieprawidłowo skonfigurowanych reguł zachowywania do reguł szczegółowych i wąskich.

  • Po pierwsze, chcemy też wspomnieć, że R8 istnieje już od wielu lat.

  • Zdajemy sobie sprawę, że chociaż umożliwia ona optymalizację o dużym wpływie, jej konfiguracja wymaga dużej uwagi.

  • Wywołania refleksyjne oznaczają, że dane wejściowe są nieprzewidywalne i mogą prowadzić do awarii w czasie działania.

  • W przeszłości zespoły odpowiedzialne za aplikacje i biblioteki były kuszone, aby dodawać zbyt szerokie reguły zachowywania, aby zachować bezpieczeństwo.

  • Wiemy, że 41% najpopularniejszych aplikacji nie osiąga poziomu optymalizacji 25% określonego przez Google Play, ale wiemy też, że 13% najpopularniejszych aplikacji osiąga już 3-krotność tego progu.

  • Taki wynik osiągają aplikacje z różnych branż i na różnych etapach ścieżki użytkownika.

  • Wysokie wyniki optymalizacji są możliwe do osiągnięcia, o ile masz odpowiednie narzędzia i widoczność, aby zarządzać zarówno własnymi regułami zachowywania, jak i tymi odziedziczonymi z bibliotek.

  • Zanim przejdziemy do nowych narzędzi i ulepszeń, które pomogą w tym procesie, chcielibyśmy też zwrócić uwagę na kilka innych ulepszeń R8 z ostatniego roku, które również ułatwiają optymalizację.

  • Po pierwsze, nowe szablony aplikacji zawierają zakres pakietu, który jest przydatny do zapewnienia minimalnego poziomu optymalizacji od samego początku.

  • Analizator konfiguracji R8, o którym za chwilę opowiemy, jest teraz obsługiwany w starszych wersjach AGP.

  • Jest też nieoficjalnie obsługiwany w Bazel.

  • W AGP 9.2 elementy podstawowe współprogramów są 2 razy szybsze.

  • Dzięki ulepszeniom R8 wprowadzonym w AGP 9.2 po uaktualnieniu z wersji 9.0.1 Tinder odnotował spadek liczby błędów ANR o 13%.

  • AGP 9.3 wprowadza też przeprojektowany interfejs API R8 z bardziej przejrzystym nazewnictwem, prostszymi ustawieniami domyślnymi reguł domyślnych i zmniejszania zasobów oraz obsługą katalogu źródłowego AGP w przypadku reguł zachowywania.

    android {
        buildTypes {
            release {
                optimization {
                    // no need to specify default
                    // rules and resource shrinking
                    enabled = true
                }
            }
        }
        ...
    }
    android {
        buildTypes {
            release {
                // Enables code optimizations.
                isMinifyEnabled = true
    
                // Enables resource shrinking.
                isShrinkResources = true
    
                proguardFiles(
                    // Default rules.
                    getDefaultProguardFile(
                        "proguard-android-optimize.txt"
                    ),
                    ... // list out each file with keep rules
                )
                ...
            }
        }
        ...
    }
  • Aktualizacje, które dzisiaj omówimy, również ułatwią pracę deweloperom.

  • Zaczniemy od tego, jak wykorzystać analizator konfiguracji R8 i umiejętność R8, aby zidentyfikować obciążenie związane ze starszymi regułami i określić ścieżkę ulepszeń.

  • Następnie Chris omówi, jak przejść od ogólnych reguł zachowywania z symbolami wieloznacznymi do nowych, precyzyjnych, wygenerowanych automatycznie adnotacji z zabezpieczeniami wpisywania.

  • Na koniec przyjrzymy się, jak używać nowych narzędzi do sprawdzania poprawności w interfejsie wiersza poleceń, aby zapewnić agresywną optymalizację bez powodowania awarii.

Omówienie analizatora konfiguracji R8

  • Analizator konfiguracji R8 to idealne miejsce na rozpoczęcie analizy optymalizacji DEX.

  • Jeśli nie korzystasz z tego narzędzia, wygląda ono tak.

  • Jest to raport HTML, który wizualizuje informacje o konfiguracji R8.

  • Ten raport dotyczy mojej aplikacji, która zawiera bibliotekę o nazwie Analytics Bloat.

  • Jak się domyślasz, nie jest to prawdziwa aplikacja ani prawdziwa biblioteka, ale jest ona wyróżniona na niebiesko u góry.

  • Analizator konfiguracji mierzy: zmniejszanie, czyli odsetek klas, metod i pól, których zmniejszaniu nie zapobiegają reguły zachowywania.

  • Jest on obliczany po pierwszym przejściu R8.

  • Mierzy też optymalizację, czyli odsetek kodu, który można optymalizować w całym programie, np. przez wstawianie metod i scalanie klas.

  • oraz zaciemnianie, czyli odsetek elementów, które można zaciemnić lub zmienić ich nazwy na krótsze identyfikatory.

  • Chcę też wspomnieć, że ponieważ Analizator konfiguracji R8 oblicza te wyniki przed ostateczną optymalizacją i kompilacją DEX, wartości optymalizacji, zmniejszania rozmiaru i zaciemniania kodu, które widzisz w tym raporcie, nie będą dokładnie odpowiadać wartościom widocznym w Konsoli Play.

  • Dzieje się tak, ponieważ Google Play mierzy końcowe dane wyjściowe DEX.

  • Powodem tej rozbieżności jest zapewnienie dostępu do szybkiej, lokalnej pętli informacji zwrotnych podczas programowania.

  • Wyniki, które widzisz w analizatorze konfiguracji R8, są jednak bardzo przydatne do sprawdzania potencjału optymalizacji plików DEX przed przesłaniem ich do Sklepu Play.

  • Raport zawiera też tabelę Analiza reguł przechowywania, która jest głównym widokiem wyświetlanym po otwarciu raportu.

  • Możesz kliknąć dowolną regułę i zobaczyć dokładną liczbę klas, metod i pól, które są przez nią zachowywane. Dzięki temu dowiesz się, które reguły zachowują więcej, niż powinny.

  • Zawiera też reguły podrzędne, które są tu powiększone.

  • Są to reguły, które pokrywają się z szerszą regułą.

  • Są one wyróżnione, dzięki czemu możesz usunąć ogólne słowo kluczowe i pozostawić tylko to bardziej szczegółowe.

  • Pokazuje też nieużywane reguły, czyli takie, które można usunąć bez ryzyka dla stabilności aplikacji.

  • Analizator konfiguracji został zaprojektowany, aby rozwiązać kilka problemów, zaczynając od nagromadzenia starszych reguł.

  • Wcześniej, bez widoczności poszczególnych reguł, usuwanie starych reguł było ryzykowne.

  • Zespoły zostawiały je więc w spokoju lub dodawały szersze symbole wieloznaczne.

  • Po drugie, pomaga wykryć luki w regułach biblioteki.

  • Dlatego gdy dodajesz pakiet SDK, trudno jest stwierdzić, czy reguła dołączona do tej biblioteki jest zbyt ogólna lub czy jest w ogóle potrzebna, na przykład po 10 wersjach.

  • Za chwilę omówimy też biblioteki.

  • Po trzecie, pomaga ocenić wpływ optymalizacji.

  • Wcześniej nie było łatwego sposobu na sprawdzenie, czy reguła zachowuje 2 czy 2000 klas.

  • Zobaczmy, jak działa R8 w kontekście mojej aplikacji i biblioteki, którą wcześniej sprawdzaliśmy.

  • Mam tu moduł aplikacji i 3 biblioteki.

  • Zwróć szczególną uwagę na bibliotekę 3 po prawej stronie.

  • Jest to biblioteka Analytics Bloat, która zawiera regułę zachowywania dla całego pakietu.

  • Najpierw R8 identyfikuje główny punkt wejścia ze wszystkich scalonych reguł zachowywania, które są tu zaznaczone na żółto.

  • Zazwyczaj powinny to być główne punkty wejścia aplikacji, takie jak klasy w pliku manifestu.

  • Z powodu reguły zachowywania symboli wieloznacznych w bibliotece Analytics Bloat każda klasa w bibliotece jest traktowana jako punkt wejścia.

  • Następnie R8 śledzi osiągalność w grafie wywołań, zaczynając od tych korzeni.

  • Każda klasa, do której można dotrzeć z elementu głównego, jest oznaczona na zielono jako przeznaczona do zachowania, a klasy, do których nie ma odwołań, są klasyfikowane jako martwy kod i oznaczone na czerwono.

  • Następnie R8 przeprowadza eliminację martwego kodu i usuwa nieużywany kod.

  • Po eliminacji martwego kodu, czyli pierwszym etapie zmniejszania, pozostaje tylko to.

  • Nasza aplikacja ma wynik zmniejszania na poziomie 42,5%, co przekracza próg 25% w Google Play, ale nadal nie jest to wynik, który by nas zadowalał.

  • Wynika to głównie z reguły zachowywania na poziomie pakietu z biblioteki Analytics.

  • Oznacza to, że tylko 17 z pozostałych 40 klas można jeszcze zmniejszyć.

  • Chcę podkreślić, że jedna nieprawidłowa reguła zachowywania może spowodować poważne problemy z optymalizacją aplikacji.

  • Mając to na uwadze, skupmy się na bibliotekach.

  • W zeszłym roku nasz zespół poświęcił dużo czasu na analizę tego, jak reguły przechowywania konsumentów biblioteki wpływają na optymalizację ekosystemu.

  • Wiele bibliotek zawiera zbyt ogólne reguły, aby zapewnić stabilność.

  • Mogą też określić ogólne reguły w dokumentacji dla deweloperów aplikacji, aby mogli oni samodzielnie dodać je do swoich aplikacji.

  • Jednak dostarczanie aplikacji z regułami ogólnymi nie jest konieczne do zapewnienia stabilności i negatywnie wpływa na wydajność, a koszty ponoszą przede wszystkim deweloperzy aplikacji.

  • Zdarzały się przypadki, gdy cała biblioteka była zachowywana przez regułę zachowywania na poziomie pakietu, np. bibliotekę bloat do statystyk. W przypadku tej biblioteki miało to daleko idący wpływ na potencjał optymalizacji aplikacji.

  • Warto też wspomnieć, że nawet nasze pakiety SDK w Google nie były odporne na ten problem. W ciągu ostatniego roku w ponad tuzinie bibliotek Google poprawiliśmy zbyt ogólne reguły przechowywania.

  • Jeśli chcesz używać analizatora konfiguracji, jest on zintegrowany bezpośrednio z AGP 9.3, ale jest też dostępny w starszych wersjach, jeśli uaktualnisz wersję R8.

  • Uruchomienie tego polecenia spowoduje wygenerowanie raportu HTML, który pokazaliśmy. Będzie on zawierał reguły zachowywania, wpływ na zmniejszanie, optymalizację i zaciemnianie kodu w przypadku poszczególnych reguł i całej aplikacji.

  • Jeśli używasz pełnej kompilacji do publikacji R8, raport jest generowany automatycznie i można go znaleźć w tej ścieżce pliku.

  • Zaletą tego samodzielnego zadania jest to, że jego wygenerowanie zajmuje mniej czasu niż pełna kompilacja.

Studium przypadku: optymalizacja WHOOP

  • Zespół WHOOP, który może być gdzieś tutaj, uruchomił analizator konfiguracji i uzyskał zmniejszenie na poziomie 74%, zaciemnienie na poziomie 74% i optymalizację na poziomie 0.

  • Jak możesz sobie wyobrazić, zespół był naprawdę zaskoczony tą wartością optymalizacji.

  • Dzięki informacjom z analizatora konfiguracji dowiedzieli się, że ich kompilacja używa starszego pliku ProGuard, który wyłącza optymalizację. Po przejściu na zaktualizowaną wersję mogli włączyć optymalizację aplikacji.

  • Dzięki temu udało się zoptymalizować 55% kodu i zmniejszyć rozmiar pliku APK o 4%.

  • W połączeniu z profilami Baseline udało się skrócić czas uruchamiania „na zimno” o 38% i ograniczyć nadmierną liczbę zawieszonych klatek o 28%.

  • Ekran po lewej stronie pokazuje aplikację bez profilu podstawowego, a ekran po prawej – z profilem. Jak widać, dodanie profilu podstawowego przyspiesza uruchamianie aplikacji.

  • Odtwórzmy to jeszcze raz.

  • Świetnie.

  • Ten link prowadzi do przewodnika po analizatorze konfiguracji. Ręczne sprawdzanie raportu jest świetnym rozwiązaniem, ale w zależności od stanu konfiguracji R8 aplikacji może to zająć dużo czasu.

  • Aby przyspieszyć ten proces, mamy umiejętność analizatora R8.

    $ android skills list
    $ android skills add r8-analyzer
  • Możesz poprosić agenta o przeanalizowanie konfiguracji R8. Nie tylko wygeneruje on raport analizatora konfiguracji R8, ale też wskaże najważniejsze reguły.

  • Oto przykładowe dane wyjściowe. Warto zwrócić uwagę na kilka szczegółów. Wynik optymalizacji wynosi 42%, co oznacza, że 58% bazy kodu nie może zostać zoptymalizowane przez R8.

  • Jeśli przyjrzymy się sekcji reguł zachowywania, reguła zachowywania na poziomie pakietu dla biblioteki bloat uniemożliwia optymalizację 22% bazy kodu. Umiejętność zaleca też doprecyzowanie tej reguły, ponieważ, jak już wiemy, jest ona zbyt szeroka.

  • Teraz możesz zdiagnozować, co powoduje nadmierny rozmiar aplikacji i blokuje optymalizację DEX, korzystając z analizatora konfiguracji R8 i umiejętności.

Wyzwania związane z refleksją w optymalizacji DEX

  • Gdy jednak znajdziesz zbyt ogólną regułę zachowywania, jak możesz ją bezpiecznie zastąpić, aby nie spowodować przypadkowo nieoczekiwanej awarii?

  • Przekazuję głos Chrisowi, który pokaże, jak przejść ze starszych reguł zachowywania na nowe, wygenerowane automatycznie adnotacje zachowywania z zabezpieczeniami wpisywania.

  • Dziękuję.

  • Porozmawiajmy o tym, jak do tego doszło.

  • W przypadku tej przeładowanej biblioteki analitycznej, ze wszystkimi tymi regułami zachowywania, które – z tą regułą zachowywania, która zachowuje w zasadzie wszystko, głównym problemem, na który natrafiamy, jest refleksja.

  • Przyjrzyjmy się kodowi refleksyjnemu, aby przypomnieć sobie, jak to działa.

    fun createWithReflection(className: String): MyObject {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as MyObject
    }
  • Jest to funkcja tworzenia instancji obiektu, która po prostu rozszerza konkretny obiekt i robi to refleksyjnie.

  • Wadą tego rozwiązania w kontekście optymalizacji DEX jest to, że opóźnia ono wszystkie te wywołania statyczne do czasu działania.

  • Jeśli spojrzymy na kod, to w zależności od tego, skąd pochodzi nazwa klasy, nasz optymalizator R8 nie jest w stanie zobaczyć, co się dzieje.

  • Są to sytuacje, które domyślnie uniemożliwiają optymalizację Dex, więc zwykle trzeba dodać regułę zachowania.

  • Najlepiej byłoby tego uniknąć, prawda?

  • Najlepiej byłoby całkowicie unikać refleksji, zwłaszcza teraz, w 2026 r., gdy mamy niesamowite narzędzia do generowania kodu na potrzeby serializacji i wstrzykiwania zależności, które mogą całkowicie wyeliminować tę kategorię problemów.

  • Ale to nie wszystko, prawda?

  • W niektórych przypadkach refleksja jest nadal konieczna.

  • Przyjrzyjmy się, jak to się stało w przypadku tej przeładowanej biblioteki.

  • Oto konkretny przykład, który jest powszechny – zależność opcjonalna.

    // Problem 1 - optional dependency
    fun tryInitCrashReporter(context: Context): EventTransport? {
       return runCatching {
           Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
               .getDeclaredConstructor(Context::class.java)
               .newInstance(context) as? EventTransport
       }.getOrNull()
    }
    
    // Problem 2 - dynamically instantiate subclass
    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
    
    // Problem 3 - serialization
    fun serializeEvent(event: UserActionEvent): JSONObject {
       val json = JSONObject()
       for (field in event.javaClass.declaredFields) {
           field.isAccessible = true
           json.put(field.name, field.get(event))
       }
       return json
    }
  • W tym przypadku rozwijamy konkretny typ, który nie jest dostępny w ścieżce klasy biblioteki, więc musimy to zrobić za pomocą ciągu znaków.

  • Odwołujemy się do tej konkretnej klasy, która może, ale nie musi być obecna w momencie tworzenia biblioteki, więc biblioteka ta używa refleksji, aby znaleźć element w powiązanej bibliotece.

  • Innym przypadkiem jest dynamiczne tworzenie instancji podklas.

  • Dlatego, zwłaszcza jeśli coś pochodzi z pliku manifestu lub jest wysyłane po stronie serwera, informujemy lokalną aplikację: „Hej, rozszerz to, utwórz to później”.

  • Trzecia kategoria, którą tu widzimy, to serializacja.

  • Mam na myśli serializację JSON, która odbywa się przez iterację wszystkich elementów.

  • To absolutnie obrzydliwe, ale takie rzeczy czasem się zdarzają.

  • Nie zawsze jest naprawiany w odpowiednim momencie.

  • Ale porozmawiajmy o tym, jaki jest tego efekt.

  • W miarę jak te wywołania refleksyjne się gromadzą, masz coraz większą motywację, aby pójść na łatwiznę i zoptymalizować aplikację pod kątem wygody dewelopera, a nie użytkownika.

  • To nie jest zbyt dobre rozwiązanie, więc dodam zadanie i będzie dobrze.

    # TODO: clean this up
    -keep com.example.analytics.bloat.** { * };
  • Pewnego dnia przyjdę i to naprawię, prawda?

  • Porozmawiajmy o tym, co powinniśmy zrobić, a przynajmniej co powinniśmy byli zrobić przed dzisiejszym dniem, i jak napisać idealną regułę Keep.

    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
  • OK, zapnij pasy.

  • Pierwsza część to zachowanie konkretnej klasy, którą zwracamy z tej funkcji refleksyjnej.

    ### Keep the EventFilter
    -keep class com.example.analytics.bloat.EventFilter {
        *
    }
  • A nie, czekaj.

  • Wiesz co?

  • To nie jest prawda.

  • To jest klasa Kotlin, muszę się upewnić, że jest to typ ciągu znaków, dodam tam KT.

    ### Keep the actual EventFilter
    -keep class com.example.analytics.bloat.EventFilterKt {
        *
    }
  • Ważne jest to, że nie zwracam obiektu EventFilter, ale podklasę EventFilter, więc muszę zachować klasy, które rozszerzają EventFilter, aby móc prawidłowo przeprowadzić inflację.

    ### Keep the EventFilter subclasses
    -keep class ** extends com.example.analytics.bloat.EventFilterKt {
        *
    }
  • Jeśli chcę to zrobić wydajniej, to rozwiązanie by zadziałało, ale żeby było jeszcze wydajniej, co powinienem zrobić jako dobry programista bibliotek?

  • Powinienem powiedzieć -keepclasseswithmembers i zachować tylko te klasy, które mają ten konkretny konstruktor <init>, którego używam do rozszerzenia obiektu.

    ### Keep the EventFilter subclasses, if they have this constructor
    -keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
       void <init>();
    }
  • Jeśli chcę być naprawdę miły, dodam allowaccessmodification, aby umożliwić jeszcze dalszą optymalizację wszystkich interfejsów API, które są objęte tą regułą Keep, ponieważ nie chcę uniemożliwiać dalszej optymalizacji przez R8. Chcę być dobrym obywatelem.

    ### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
    -keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
       void <init>();
    }
  • Ostatni krok, którego prawie nikt nie wykonuje, ponieważ wymaga dodatkowej pracy, ale jest świetny w przypadku biblioteki, to uczynienie jej warunkową.

    ### -if makes this conditional, critical for libraries
    -if class com.example.analytics.bloat.EventToolsKt {
        class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
    }
    ### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
    -keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
        void <init>();
    }
  • Koncepcyjnie nie musisz zachowywać wszystkich tych podklas, chyba że faktycznie wywołujesz tę funkcję. To właśnie robi instrukcja if na początku reguły Keep.

  • Mógłbym poprosić o podniesienie rąk, ale wiem, że prawie nikt tego nie robi.

  • Na przykład faktycznie go napisać, uczynić go warunkowym.

  • A gdybyśmy mogli to trochę ulepszyć?

  • Let's Let's first look at see, like, okay.

  • Może da się to zrobić inaczej.

  • Omówiliśmy już, co się stanie, jeśli napiszę wąską, ręcznie napisaną regułę Keep.

  • Technicznie jest to możliwe, ale trudno jest to napisać, trudno odczytać i bardzo trudno utrzymać, ponieważ jest tam wiele małych fragmentów z typem ciągów znaków, które łatwo uszkodzić, gdy coś się pod nimi wydarzy.

  • Pod spodem coś się zmienia.

  • Mamy też alternatywy.

  • Mamy adnotacje takie jak @Keep.

  • Adnotacja @Keep jest w pakiecie adnotacji AndroidX od dawna, ale obecnie nie zaleca się jej używania.

    @androidx.annotation.Keep
    class EventFilter { … }
    
    @androidx.annotation.Keep
    class SizeBasedEventFilter { … }
    
    @androidx.annotation.Keep
    class DropLogsEventFilter { … }
  • Nie jest to najlepsze rozwiązanie dla deweloperów bibliotek, a w zasadzie jest ono wewnętrznie zakazane, ponieważ choć jest łatwiejsze w utrzymaniu, jest bardzo nieelastyczne i nie zawiera niektórych optymalizacji, o których mówiłem wcześniej.

  • Nie może.

  • Nie pozwala na analizę miejsca wywołania.

  • Wróćmy do poprzedniego przykładu: „Mogę po prostu dodać @Keep do EventFilter i będzie dobrze, prawda?”.

    @Keep
    class EventFilter { … }
  • No nie.

  • Adnotacja @Keep nie jest kierowana na podklasy, tylko na tę klasę.

    @Keep
    class EventFilter { … }
    
    @Keep
    class SizeBasedEventFilter { … }
    
    @Keep
    class DropLogsEventFilter { … }
  • Muszę więc kierować na wszystkie podklasy i teraz piszę więcej adnotacji @Keep, niż gdybym użył tej jednej reguły Keep, którą trudno było napisać.

  • Oba te rozwiązania mają więc swoje wady.

  • Łatwo o nich zapomnieć, dowiadujesz się o nich późno i domyślnie nie są zależne od dzwoniącego.

  • Chcemy, aby biblioteki miały lepsze rozwiązanie.

  • Chcemy, aby można było łatwiej uzyskać odpowiednie wyniki od razu po uruchomieniu.

  • Jednym z głównych problemów, które tu widzimy, jest to, że te elementy znajdują się daleko od odbicia, przed którym faktycznie chronimy.

  • Ostatecznie chcemy, aby rozwiązanie znajdowało się tuż obok tego nieprawidłowego kodu odbicia, który faktycznie zawyża docelowe elementy.

Wprowadzenie do Jetpack Keep

  • Zacznijmy od Jetpack Keep, nowej biblioteki i wtyczki z AndroidX, która ma pomóc w rozwiązaniu tego problemu.

  • Celem Jetpack Keep jest przede wszystkim generowanie reguł zachowywania na podstawie adnotacji, ale też generowanie adnotacji bezpośrednio z refleksji, która jest już w bazie kodu.

  • Konfiguracja jest więc bardzo prosta.

    plugins {
       id("com.android.library")
       id("androidx.annotation.keep")
    }
    
    dependencies {
        implementation("androidx.annotation.keep:keep")
        ...
    }
  • To wtyczka Gradle i biblioteka adnotacji.

  • Zobaczmy, jak to działa w czasie kompilacji.

  • Jeśli mam poprzedni przykład, w którym rozwijam podklasę znanego typu, co muszę zrobić?

  • Wystarczy, że uruchomię lintFix.

    ./gradlew analyticsbloat:lintFix
  • Lint analizuje wszystkie refleksje w tej metodzie i informuje, że potrzebna jest ta adnotacja.

    @UsesReflectionToConstruct(
       classConstant = EventFilter::class,
       parameterTypes = []
    )
    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
  • Ta adnotacja dokładnie opisuje, co robi refleksja w tym wywołaniu.

  • Oznacza to: „Rozwijam klasę tego typu”.

  • W tym konkretnym przypadku EventFilter jest znaną klasą.

  • Nie używam ciągu znaków, aby się do niego odwołać, ponieważ nie muszę tego robić.

  • To dodatkowe zabezpieczenie, a nawet koduję parameterTypes, aby dokładnie określić, który konstruktor jest zachowywany.

  • Co to oznacza?

  • Jak to wpływa na ostateczną kompilację R8?

  • Wygeneruje regułę zachowania, która jest dokładnie taka sama jak ta, którą mieliśmy wcześniej.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.EventTools {
        class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
    }
    -keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
        void <init>();
    }
  • Jest on kierowany na dokładnie te same klasy i podklasy oraz jest zależny od tego punktu odbicia.

  • Nie muszę też dbać o dodawanie adnotacji do wszystkich podklas.

  • Nie muszę przechowywać osobnego pliku z regułą przechowywania w innym miejscu.

  • To znacznie łatwiejszy sposób zarządzania regułami przechowywania.

  • Wróćmy do przykładu z zależnością opcjonalną.

    @UsesReflectionToConstruct(
       className = "com.example.analytics.bloat.crash.CrashReporterTransport",
       parameterTypes = [Context::class]
    )
    fun tryInitCrashReporter(context: Context): EventTransport? {
       return runCatching {
           Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
               .getDeclaredConstructor(Context::class.java)
               .newInstance(context) as? EventTransport
       }.getOrNull()
    }
  • W tym przypadku klasa nie znajdowała się w ścieżce klasy, ale nadal mogę się do niej odwoływać.

  • Ja używam do tego ciągu znaków, ale wszystko inne, czyli typy parametrów, można odwoływać się do nich za pomocą odwołań do klas, więc tak robię.

  • Ponownie wygenerujemy idealną regułę przechowywania dla tego przypadku.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.CrashConnection {
        class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
    }
    -keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
        void <init>(android.content.Context);
    }
  • Jest warunkowa, konkretna i ukierunkowana oraz umożliwia optymalizację wszędzie tam, gdzie jest to możliwe, w przypadku tych ukierunkowanych klas.

  • Oto refleksja nad polami. W tym konkretnym przypadku iterowałem wszystkie pola wewnątrz obiektu, ale tutaj również radzimy sobie z tym, mówiąc, że chcemy kierować na wszystkie pola za pomocą operatora gwiazdki.

    // TODO: migrate to Kotlinx Serialization or Moshi codegen.
    @UsesReflectionToAccessField(
       classConstant = UserActionEvent::class,
       fieldName = "*"
    )
    fun serializeEvent(event: UserActionEvent): JSONObject {
       val json = JSONObject()
       for (field in event.javaClass.declaredFields) {
           field.isAccessible = true
           json.put(field.name, field.get(event))
       }
       return json
    }
  • To pozwala mi kierować na wszystkie pola. Jak widać, generuje to dokładnie taką regułę zachowania, jakiej można by się spodziewać w przypadku tego, no cóż, niesamowicie niefortunnego odbicia, którego nie powinno być w mojej aplikacji. Ale obiecuję, że wrócę i to naprawię.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.serialization.EventSerializer {
        class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
    }
    -keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
        <fields>;
    }
  • Jak działa Jetpack Keep?

  • Znacznie ogranicza to konieczność zarządzania regułami przechowywania.

  • Wszystko, co można wygenerować automatycznie, nie wymaga już Twojej uwagi ani nie stanowi ryzyka związanego z utrzymywaniem reguł przechowywania.

  • Zapewnia też determinizm linta.

  • Nie tylko naprawia błędy podczas iteracji i prób migracji, ale też wykrywa nowe refleksje i zmiany refleksyjne przy każdej zmianie kodu, a to bezpieczeństwo jest absolutnie kluczowe.

  • Czy mogę z tego korzystać?

  • Jakie są więc zależności?

  • Wtyczka i narzędzie lint wymagają AGP 8.7.

  • To powinni być prawie wszyscy.

  • Możesz też używać go w bardzo elastyczny sposób.

  • Możesz używać tej funkcji w aplikacjach.

  • Możesz używać tego rozwiązania w bibliotekach Androida, plikach AAR, bibliotekach KMP i plikach JAR – w tym przypadku wymaga to trochę ręcznego podłączania.

  • Najważniejsze jest jednak to, że jest to bezpieczne dla dewelopera biblioteki.

  • Nawet jeśli ktoś używa bardzo starej wersji R8 lub narzędzia innej firmy, które korzysta z Twojej biblioteki, nadal eksportujemy rzeczywiste standardowe reguły konsumenckie, aby każdy mógł korzystać z Twojej biblioteki i nie musiał się martwić o używanie nowego, niezgodnego formatu.

Analizowanie aplikacji za pomocą interfejsu wiersza poleceń Androida

  • Skoro już omówiliśmy, jak Jetpack Keep może pomóc w optymalizacji i ochronie wywołań kodu refleksyjnego, porozmawiajmy o nowych funkcjach, które niedawno wprowadziliśmy w interfejsie wiersza poleceń Androida, aby umożliwić podwójne sprawdzenie na ostatnim etapie publikacji.

  • Breana wspomniała wcześniej, że w sekcji Wyniki w Google Play znajdziesz informacje o rozmiarze aplikacji, rozmiarze DEX i procentach optymalizacji.

  • Teraz zaczęliśmy udostępniać tę funkcję również w interfejsie wiersza poleceń Androida.

  • Wykorzystuje dokładnie tę samą logikę co Sklep Play, więc nie musisz przesyłać pakietu do Sklepu Play, aby go przeanalizować – możesz to zrobić za pomocą interfejsu wiersza poleceń.

    $ android analyze <mybundle.aab>
    $ android analyze <mybundle.apk>
  • Te same informacje otrzymasz bezpośrednio i natychmiast dzięki analizie lokalnej.

  • W tym konkretnym przypadku aplikacja otrzymuje ostrzeżenie o bardzo niskim poziomie optymalizacji. Otrzymuje też informację o rozmiarze plików DEX i procentowych wskaźnikach optymalizacji, dokładnie tak jak w Sklepie Play.

  • Mamy też dane wyjściowe w formacie JSON, więc jeśli chcesz napisać skrypt do automatyzacji testów regresji pod kątem drastycznego spadku którejkolwiek z tych wartości, ponieważ są to liczby, które Play egzekwuje, i chcesz je śledzić, możesz to łatwo zrobić za pomocą tych raportów JSON.

    {
      "optimization": {
        "bundleAnalysisResult": {
          "agpVersion": "9.3.2",
          "r8Json": {
            "version": "9.3.16",
            "options": {
              "isRepackageClassesEnabled": true,
              "isProguardCompatibilityModeEnabled": false
            },
            "stats": {
              "shrinkingRatio": 0.2306,
              "optimizationRatio": 0.2351,
              "obfuscationRatio": 0.2322
            },
            ...
          }
        "mappingObfuscationRatio": 0.21631891,
        "dexHeuristicObfuscationRatio": 0.25118626,
        "uncompressedDexSizeBytes": "16198152"
      }
  • Widzimy, że te same liczby są również obecne tutaj.

  • To nie jest jedyna funkcja android analyze.

  • android analyze można też uruchamiać w bibliotekach.

    $ android analyze <library.aar>
    $ android analyze <library.jar>
  • Jak wspomniała Breana, istnieje wiele problemów z bibliotekami, które są dostarczane z nieodpowiednimi regułami zachowywania.

  • Wykorzystajmy to, aby przyjrzeć się niektórym z tych problemów.

  • Gdy uruchamiam to narzędzie na konkretnym pliku AAR, widzę, że ta biblioteka ma kilka problemów, które są wyróżnione na samej górze.

  • Ta biblioteka ma wysoki współczynnik zachowywania, co oznacza, że zachowuje zbyt wiele danych z powodu nieograniczonych reguł przechowywania.

  • Zawiera ogólne reguły zachowywania, które uniemożliwiają optymalizację. Poniżej znajdują się liczby, które pokazują, jak bardzo jest to niekorzystne. Zawiera też reguły zachowywania, które, co może być najbardziej przerażające, nie są kierowane na nic, co prawdopodobnie oznacza, że są kierowane na kogoś innego w nieograniczony sposób.

  • Bardzo niebezpieczne zapachy kodu.

  • Możesz więc sprawdzić to wszystko w pliku AAR. Jest to świetny sposób, zwłaszcza dla wydawcy biblioteki, aby upewnić się, że nie dostarczasz nieprawidłowych reguł zachowywania i nie dopuszczasz do ich wycieku, gdy nad biblioteką pracuje ktoś, kto nie jest ekspertem w pisaniu reguł zachowywania.

  • Tutaj widać, że podajemy też współczynnik zachowania.

  • To dobry sposób na ocenę ogólnej jakości biblioteki.

  • Jeśli zoptymalizuję pojedynczą bibliotekę całkowicie samodzielnie, bez odwołań, za pomocą R8, powinna ona mieć 0 bajtów, prawda?

  • Powinna być pusta, ponieważ nic w niej nie jest przechowywane.

  • To byłoby idealne rozwiązanie.

  • W tym przypadku tak nie jest.

  • Możesz bezpośrednio sprawdzić rozmiar R8 i D8.

  • Wyróżniamy też niektóre z najgorszych reguł przechowywania, aby były bardziej widoczne.

  • Ma on również format JSON, więc niezależnie od tego, czy jesteś deweloperem aplikacji, który próbuje kontrolować aktualizacje biblioteki, czy deweloperem biblioteki, który próbuje zapobiec dużym regresjom w jej jakości, możesz przeprowadzić tę samą analizę i zapobiec tym problemom za pomocą prostego skryptu, który opakowuje interfejs wiersza poleceń Androida.

    {
      "optimization": {
        "libraryAnalysisResult": {
          "dex": {
            "d8CompileResult": {
              "dexSizeBytes": 4523352
            },
            "r8CompileResult": {
              "dexSizeBytes": 3266596,
              "configuration": {
                "usedPackageWideKeepRules": [
                  {
                    "source": "-keep class com.example.analytics.bloat.** { *; }",
                    "keptItemCount": 125579,
                  },
                  ...
                ],
                "unusedPackageWideKeepRules": [
                  {
                    "source": "-keep class some.other.library.** { *; }",
                  },
                  ...
                ]
              },
              ...
    }
  • Jeśli więc znajdziesz problem z biblioteką, najpierw spróbuj ją zaktualizować.

  • Jak wspomniała wcześniej Breana, poprawiliśmy kilkanaście bibliotek Google, aby naprawić niektóre bardzo niefortunne reguły zachowywania. Istnieją też inne biblioteki, które zostały poprawione, ale nie są aktualizowane przez aplikacje, które ich używają.

  • To świetny pierwszy krok.

  • Po drugie, możesz poszukać problemu i skontaktować się z deweloperem biblioteki, a następnie dołączyć raport z analizatora, który informuje, że zachowuje on zbyt wiele danych.

  • Pomóż mi zoptymalizować aplikację”.

Najważniejsze wnioski i zasoby

  • Podsumowując, omówiliśmy analizator konfiguracji R8, który pomaga przeanalizować wszystko, co dzieje się w kompilacji R8, i wszystko, co informuje o tym, co można zoptymalizować, a czego nie.

  • Omówiliśmy Jetpack Keep, nową bibliotekę, która pomaga automatycznie pisać reguły zachowywania bez konieczności wykonywania dodatkowych czynności.

  • oraz interfejs wiersza poleceń Androida, czyli nowe narzędzie, które pomoże Ci sprawdzić i upewnić się, że wszystko jest w porządku, i że Ty, Twój agent lub młodszy inżynier w Twoim zespole nie zepsuł niczego dla Twoich klientów.

  • Jeśli chcesz dowiedzieć się więcej, zapoznaj się z dokumentacją R8, analizatora konfiguracji i Jetpack Keep.

  • W dokumentacji interfejsu wiersza poleceń Androida znajdziesz informacje o poleceniu analyze i sposobie jego użycia.

  • Bardzo dziękuję.

Kod

Fragmenty kodu

Fragment skryptu Kotlin w 05:15 w filmie „Wprowadzenie do optymalizacji DEX”
android {
    buildTypes {
        release {
            optimization {
                // no need to specify default
                // rules and resource shrinking
                enabled = true
            }
        }
    }
    ...
}
Fragment skryptu Kotlin w 05:15 w filmie „Wprowadzenie do optymalizacji DEX”
android {
    buildTypes {
        release {
            // Enables code optimizations.
            isMinifyEnabled = true

            // Enables resource shrinking.
            isShrinkResources = true

            proguardFiles(
                // Default rules.
                getDefaultProguardFile(
                    "proguard-android-optimize.txt"
                ),
                ... // list out each file with keep rules
            )
            ...
        }
    }
    ...
}
Fragment kodu powłoki w 13:19 w filmie „Studium przypadku: optymalizacja WHOOP”
$ android skills list
$ android skills add r8-analyzer
Fragment kodu w języku Kotlin w 14:41 w filmie „Wyzwania związane z refleksją w optymalizacji DEX”
fun createWithReflection(className: String): MyObject {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as MyObject
}
Fragment kodu w języku Kotlin w 15:40 w filmie „Challenges of reflection in DEX optimization”
// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
   return runCatching {
       Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
           .getDeclaredConstructor(Context::class.java)
           .newInstance(context) as? EventTransport
   }.getOrNull()
}

// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}

// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
   val json = JSONObject()
   for (field in event.javaClass.declaredFields) {
       field.isAccessible = true
       json.put(field.name, field.get(event))
   }
   return json
}
Fragment kodu powłoki w 16:52 w filmie „Wyzwania związane z refleksją w optymalizacji DEX”
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };
Fragment kodu w języku Kotlin w 16:59 w filmie „Wyzwania związane z refleksją w optymalizacji DEX”
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
Fragment kodu powłoki w 17:07 w filmie „Challenges of reflection in DEX optimization”
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
    *
}
Fragment kodu powłoki w 17:15 w filmie „Wyzwania związane z refleksją w optymalizacji DEX”
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
    *
}
Fragment kodu powłoki w 17:19 w filmie „Challenges of reflection in DEX optimization”
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
    *
}
Fragment kodu powłoki w 17:40 w filmie „Wyzwania związane z refleksją w optymalizacji DEX”
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
Fragment kodu powłoki w 17:50 w filmie „Wyzwania związane z refleksją w optymalizacji DEX”
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
Fragment kodu powłoki w 18:03 w filmie „Challenges of reflection in DEX optimization”
### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
    class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
    void <init>();
}
Fragment kodu w języku Kotlin w 18:57 w filmie „Wyzwania związane z refleksją w optymalizacji DEX”
@androidx.annotation.Keep
class EventFilter { … }

@androidx.annotation.Keep
class SizeBasedEventFilter { … }

@androidx.annotation.Keep
class DropLogsEventFilter { … }
Fragment kodu w języku Kotlin w 19:22 w filmie „Wyzwania związane z refleksją w optymalizacji DEX”
@Keep
class EventFilter { … }
Fragment kodu w języku Kotlin w 19:29 w filmie „Wyzwania związane z refleksją w optymalizacji DEX”
@Keep
class EventFilter { … }

@Keep
class SizeBasedEventFilter { … }

@Keep
class DropLogsEventFilter { … }
Fragment skryptu Kotlin w 20:38 w filmie „Introduction to Jetpack Keep”
plugins {
   id("com.android.library")
   id("androidx.annotation.keep")
}

dependencies {
    implementation("androidx.annotation.keep:keep")
    ...
}
Fragment kodu powłoki w 20:54 w filmie „Introduction to Jetpack Keep”
./gradlew analyticsbloat:lintFix
Fragment kodu w Kotlinie w 20:55 w filmie „Introduction to Jetpack Keep”
@UsesReflectionToConstruct(
   classConstant = EventFilter::class,
   parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
Fragment kodu powłoki w 21:25 w filmie „Introduction to Jetpack Keep”
# Generated Keep Rule

-if class com.example.analytics.bloat.EventTools {
    class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
    void <init>();
}
Fragment kodu w Kotlinie w 21:52 w filmie „Introduction to Jetpack Keep”
@UsesReflectionToConstruct(
   className = "com.example.analytics.bloat.crash.CrashReporterTransport",
   parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
   return runCatching {
       Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
           .getDeclaredConstructor(Context::class.java)
           .newInstance(context) as? EventTransport
   }.getOrNull()
}
Fragment kodu powłoki w 22:05 w filmie „Introduction to Jetpack Keep”
# Generated Keep Rule

-if class com.example.analytics.bloat.CrashConnection {
    class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
    void <init>(android.content.Context);
}
Fragment kodu w Kotlinie w 22:16 w filmie „Introduction to Jetpack Keep”
// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
   classConstant = UserActionEvent::class,
   fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
   val json = JSONObject()
   for (field in event.javaClass.declaredFields) {
       field.isAccessible = true
       json.put(field.name, field.get(event))
   }
   return json
}
Fragment kodu powłoki w 22:27 w filmie „Introduction to Jetpack Keep”
# Generated Keep Rule

-if class com.example.analytics.bloat.serialization.EventSerializer {
    class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
    <fields>;
}
Shell snippet at 24:22 in "Analyzing apps with Android CLI"
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>
Fragment kodu JSON w 24:52 w filmie „Analyzing apps with interfejs wiersza poleceń Androida”
{
  "optimization": {
    "bundleAnalysisResult": {
      "agpVersion": "9.3.2",
      "r8Json": {
        "version": "9.3.16",
        "options": {
          "isRepackageClassesEnabled": true,
          "isProguardCompatibilityModeEnabled": false
        },
        "stats": {
          "shrinkingRatio": 0.2306,
          "optimizationRatio": 0.2351,
          "obfuscationRatio": 0.2322
        },
        ...
      }
    "mappingObfuscationRatio": 0.21631891,
    "dexHeuristicObfuscationRatio": 0.25118626,
    "uncompressedDexSizeBytes": "16198152"
  }
Shell snippet at 25:17 in "Analyzing apps with Android CLI"
$ android analyze <library.aar>
$ android analyze <library.jar>
Fragment kodu JSON w 26:46 w filmie „Analizowanie aplikacji za pomocą interfejsu wiersza poleceń Androida”
{
  "optimization": {
    "libraryAnalysisResult": {
      "dex": {
        "d8CompileResult": {
          "dexSizeBytes": 4523352
        },
        "r8CompileResult": {
          "dexSizeBytes": 3266596,
          "configuration": {
            "usedPackageWideKeepRules": [
              {
                "source": "-keep class com.example.analytics.bloat.** { *; }",
                "keptItemCount": 125579,
              },
              ...
            ],
            "unusedPackageWideKeepRules": [
              {
                "source": "-keep class some.other.library.** { *; }",
              },
              ...
            ]
          },
          ...
}