Tools und Leistung

Typsichere Keep-Regeln und DEX-Optimierung

Lautsprecher

Info

Über den Vortrag

Android-Apps werden oft nicht optimal bereitgestellt, weil zu konservative Beibehaltungsregeln verwendet werden, die häufig aus Drittanbieterbibliotheken stammen. Das führt zu aufgeblähten APKs, langsameren Kaltstarts und einer schlechteren Laufzeitleistung.

Mit den neuen Leistungsgrenzwerten von Google Play für die DEX-Optimierung hängt die Sichtbarkeit von Apps jetzt direkt davon ab, ob sie die Optimierungskurve einhalten.

In diesem Vortrag wird ein moderner R8-Workflow vorgestellt, mit dem sich die Regelüberfrachtung reduzieren lässt. Sie lernen Folgendes:

  • Verwenden Sie den R8-Konfigurationsanalysator und den Skill, um den Overhead durch Legacy-Regeln zu identifizieren.
  • Wechsel von allgemeinen Platzhalter-Beibehaltungsregeln zu präzisen, typsicheren, generierten Annotationen.
  • Mit den neuen Android-Befehlszeilentools können Sie Optimierungsregressionen verhindern.

Zusammenfassung

  • Google Play erzwingt Grenzwerte für die App-Qualität, die eine Codeoptimierung von mehr als 25 % erfordern. Wenn Sie diese einhalten, wird die Speichernutzung reduziert und gleichzeitig die Startgeschwindigkeit und die ANR-Rate verbessert.
  • Der R8 Configuration Analyzer generiert einen HTML-Bericht, in dem die Keep-Regeln aufgeführt sind, die Code beibehalten. Dabei werden subsumierte und nicht verwendete Regeln hervorgehoben und das Optimierungspotenzial vor der Kompilierung gemessen.
  • Zu weit gefasste Beibehaltungsregeln, die oft eingeführt werden, um Abstürze durch Reflexion zu verhindern, oder die von Bibliotheken übernommen werden, beeinträchtigen die App-Optimierung. Eine einzelne paketweite Platzhalterregel kann Dutzende ungenutzter Klassen beibehalten.
  • Jetpack Keep verwendet Lint, um Reflexion zu erkennen und präzise Annotationen und bedingte Beibehaltungsregeln direkt aus dem Code zu generieren. Es werden Standard-Consumer-Regeln exportiert, die mit älteren R8-Versionen kompatibel sind.
  • Die Android-Befehlszeile prüft App-Bundles und AARs mit der genauen Logik von Google Play, ohne dass ein Upload erforderlich ist. Es kennzeichnet problematische Regeln und exportiert JSON-Berichte für automatisierte Regressionstests.

Ressourcen

Gemini Notebook

Themen

  • dex-optimization
  • r8
  • keep-rules
  • jetpack-keep
  • android-cli
  • app-performance

Transkript

Einführung in die DEX-Optimierung

  • Hallo zusammen!

  • Mein Name ist Breana und ich bin Mitglied des Android DevRel-Teams.

  • Und ich bin Chris Craik vom Android Performance-Team.

  • Willkommen bei typsicheren Beibehaltungsregeln und der DEX-Optimierung.

  • Heute geht es darum, was die DEX-Optimierung ist, warum sie wichtig ist und welche spannenden Neuerungen es gibt.

  • Lange Zeit wurde die DEX-Optimierung als etwas angesehen, das man zwar gerne hätte, aber nicht unbedingt braucht.

  • Vielleicht konfigurieren Sie sie einmal und lassen sie dann einfach so, oder Sie nehmen sich Zeit dafür, wenn Sie etwas Zeit haben.

  • Die Mobilbranche hat jedoch mit erheblichen Engpässen bei der Hardwareversorgung zu kämpfen, die sich auf die Verfügbarkeit von Arbeitsspeicher auswirken. Daher ist eine gut optimierte DEX-Datei unerlässlich, damit Ihre App weniger Arbeitsspeicher benötigt, schneller startet, weniger ANR-Fehler verursacht und die Rendering- und Laufzeitleistung insgesamt verbessert wird.

  • Damit das Android-Ökosystem gesund bleibt und Ihre Apps weiterhin erstklassige Funktionen bieten, hat Google Play die neue Anforderung an die App-Qualität mit Leistungsgrenzwerten angekündigt, die im Februar in Kraft treten.

  • Dazu gehören auch Grenzwerte für die DEX-Codeoptimierung.

  • Apps müssen bei der Verkleinerung, Optimierung und Verschleierung einen Wert von über 25 % erreichen.

  • Was bedeutet das also für Sie als Android-Entwickler?

  • Nicht optimierter DEX-Code wirkt sich nicht nur negativ auf die Nutzerfreundlichkeit aus, sondern kann auch die Sichtbarkeit und die Veröffentlichungsmöglichkeiten Ihrer App im Google Play Store beeinträchtigen.

  • Aber Sie sind hier genau richtig. Am Ende dieses Vortrags werden Sie die Tools haben, um diese Anforderungen nicht nur zu erfüllen, sondern zu übertreffen.

  • Die DEX-Codeoptimierung kann mit R8, dem App-Optimierer von Android, erreicht werden.

  • R8 ist nicht zwingend erforderlich, wenn ein anderer App-Optimierer Ihre App über diese Schwellenwerte bringen kann. Es ist jedoch der empfohlene Weg, der direkt in die Toolchain von Android integriert ist.

  • Ein gut optimiertes DEX-App-Bundle verbraucht zwar weniger Arbeitsspeicher, die Hauptvorteile sind jedoch weiterhin leistungsbezogen: weniger ANR-Fehler, schnellerer Start und verbesserte Laufzeitleistung.

  • Die Optimierung Ihrer App hat nachweislich positive Auswirkungen auf Ihre App und Ihre Nutzer.

  • Durch die Einführung des R8-Vollmodus und die Optimierung der Keep-Regeln konnte Grab beispielsweise eine Optimierung von 78 %, eine Verkleinerung von 79 % und eine Obfuskation von 82 % für seine App erzielen.

  • Vielleicht denken Sie jetzt: „Okay, das sind ziemlich hohe Zahlen, aber was bedeutet das eigentlich?“

  • Durch diese Verbesserungen in Kombination mit Baseline-Profilen konnten ANR-Fehler um 32 % reduziert und die Startzeit um 35 % verbessert werden.

  • Wenn Top-Apps zum ersten Mal R8 verwenden, sinkt die DEX-Größe im Durchschnitt um 38 % und die von Nutzern wahrgenommenen ANR-Fehler um 16 %.

  • Und wenn Sie R8 bereits verwenden, haben wir gute Neuigkeiten für Sie.

  • Bei Apps, für die R8 bereits aktiviert ist und bei denen dann Maßnahmen zur weiteren Verbesserung der Optimierung, Minimierung und Verschleierung auf über 50 % ergriffen werden, verringert sich die DEX-Größe im Durchschnitt um weitere 25 %, die Startzeit um 4 % und die vom Nutzer wahrgenommenen ANR-Fehler um weitere 17 % – zusätzlich zu den 16 %, die wir auf der vorherigen Folie gezeigt haben.

  • Heute möchten wir Ihnen zeigen, wie Sie solche Zahlen erreichen können.

  • Wir stellen Ihnen Tools vor, mit denen Sie Ihren DEX-Code optimieren und von zu weit gefassten und falsch konfigurierten Beibehaltungsregeln zu spezifischen und engen Regeln wechseln können.

  • Zuerst möchten wir erwähnen, dass es R8 schon seit vielen Jahren gibt.

  • Wir wissen, dass die Konfiguration trotz der Möglichkeit einer hocheffektiven Optimierung viel Sorgfalt erfordert.

  • Reflektierende Aufrufe bedeuten, dass Eingaben unvorhersehbar sind und zu Laufzeitabstürzen führen können.

  • Das hat in der Vergangenheit sowohl App- als auch Bibliotheksteams dazu verleitet, zu weit gefasste Beibehaltungsregeln hinzuzufügen, um auf Nummer sicher zu gehen.

  • Wir wissen, dass 41 % der Top-Apps unter dem von Google Play definierten Optimierungsgrad von 25 % liegen, aber wir wissen auch, dass 13 % der Top-Apps bereits das Dreifache dieses Schwellenwerts erreichen.

  • Diese Zahl wird von Apps aus den unterschiedlichsten Branchen und mit den unterschiedlichsten User Journeys erreicht.

  • Und Sie können für Ihre App hohe Optimierungsfaktoren erzielen, solange Sie die richtigen Tools und die richtige Sichtbarkeit haben, um sowohl Ihre eigenen Beibehaltungsregeln als auch die aus Bibliotheken übernommenen Regeln zu verwalten.

  • Bevor wir uns die neuen Tools und Verbesserungen ansehen, die dabei helfen, möchten wir noch ein paar andere R8-Verbesserungen aus dem letzten Jahr hervorheben, die diese Optimierung ebenfalls etwas einfacher machen.

  • Erstens enthalten neue App-Vorlagen einen Paketbereich, der von Anfang an ein Mindestmaß an Optimierung gewährleistet.

  • Der R8-Konfigurationsanalysator, auf den wir gleich noch zu sprechen kommen, wird jetzt auch in älteren AGP-Versionen unterstützt.

  • Außerdem wird es inoffiziell für Bazel unterstützt.

  • Und in AGP 9.2 wurden Koroutinen-Primitive doppelt so schnell.

  • Und dank der R8-Verbesserungen in AGP 9.2 konnte Tinder nach dem Upgrade von Version 9.0.1 die Anzahl der ANR-Fehler um 13 % senken.

  • AGP 9.3 bietet außerdem eine neu gestaltete R8 API mit klareren Bezeichnungen, einfacheren Standardeinstellungen für Standardregeln und die Entfernung von Ressourcen sowie Unterstützung für AGP-Quellverzeichnisse für Keep-Regeln.

    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
                )
                ...
            }
        }
        ...
    }
  • Die Updates, die wir heute vorstellen, verbessern auch die Entwicklungsumgebung.

  • Zuerst sehen wir uns an, wie Sie mit dem R8 Configuration Analyzer und dem R8-Skill den Overhead von Legacy-Regeln ermitteln und einen Weg zur Verbesserung finden können.

  • Anschließend erklärt Chris, wie Sie von allgemeinen Platzhalter-Beibehaltungsregeln zu den neuen präzisen, typsicheren, automatisch generierten Annotationen wechseln.

  • Zum Schluss sehen wir uns an, wie Sie mit den neuen CLI-Validierungstools eine aggressive Optimierung ohne Abstürze erreichen.

R8 Configuration Analyzer – Übersicht

  • Der R8 Config Analyzer ist der ideale Ausgangspunkt für die DEX-Optimierung.

  • Wenn Sie das Tool noch nicht verwendet haben, sieht es so aus.

  • Es handelt sich um einen HTML-Bericht, in dem Informationen zu Ihrer R8-Konfiguration visualisiert werden.

  • Dieser Bericht ist für meine App, die eine Bibliothek namens „Analytics Bloat“ enthält.

  • Wie Sie sich denken können, ist dies keine echte App oder Bibliothek, aber sie wird oben blau hervorgehoben.

  • Der Config Analyzer misst Folgendes: Minimierung, d. h. den Prozentsatz der Klassen, Methoden und Felder, die R8 aufgrund von Keep-Regeln nicht minimieren kann.

  • Dies wird berechnet, nachdem R8 den ersten Verkleinerungsdurchlauf durchgeführt hat.

  • Außerdem wird die Optimierung gemessen, d. h. der Prozentsatz des Codes, der für Optimierungen des gesamten Programms wie Method-Inlining und Klassenzusammenführung offen ist.

  • Und die Verschleierung, d. h. der Prozentsatz der Mitglieder, die verschleiert oder in kürzere Kennungen umbenannt werden können.

  • Da der R8 Config Analyzer diese Werte vor den endgültigen Optimierungen und der DEX-Kompilierung berechnet, stimmen die Werte für Optimierung, Verkleinerung und Verschleierung in diesem Bericht nicht genau mit den Werten in der Play Console überein.

  • Das liegt daran, dass Google Play die endgültige DEX-Ausgabe misst.

  • Der Grund für diese Diskrepanz ist, dass Sie bei der Entwicklung auf einen schnellen, lokalen Feedback Loop zugreifen können.

  • Die im R8 Config Analyzer angezeigten Werte sind jedoch sehr hilfreich, um das Potenzial für die DEX-Optimierung zu prüfen, bevor Sie Ihre App in den Google Play Store hochladen.

  • Der Bericht enthält auch die Tabelle „Analyse der Aufbewahrungsregel“. Das ist die Hauptansicht, die Sie sehen, wenn Sie den Bericht aufrufen.

  • Dort können Sie auf eine beliebige Regel klicken, um die genaue Anzahl der Klassen, Methoden und Felder zu sehen, die durch eine bestimmte Regel beibehalten werden. So können Sie sehen, welche Regeln mehr beibehalten, als sie sollten.

  • Außerdem enthält sie untergeordnete Regeln, die hier herangezoomt werden.

  • Diese Regeln überschneiden sich mit einer allgemeineren Regel.

  • Diese werden hervorgehoben, damit Sie die weit gefasste entfernen und nur die spezifischere behalten können.

  • Außerdem werden nicht verwendete Regeln angezeigt, die ohne Risiko für die Stabilität Ihrer App gelöscht werden können.

  • Der Config Analyzer wurde entwickelt, um einige Probleme zu beheben, angefangen bei der Anhäufung von Legacy-Regeln.

  • Bisher war es ohne Sichtbarkeit auf Regelebene riskant, alte Regeln zu löschen.

  • Daher haben Teams sie oft ignoriert oder einfach allgemeine Platzhalter hinzugefügt.

  • Zweitens: Sie können damit blinde Flecken in den Bibliotheksregeln erkennen.

  • Wenn Sie ein SDK hinzufügen, ist es daher schwierig, festzustellen, ob eine Regel, die mit dieser Bibliothek gebündelt ist, zu allgemein ist oder ob sie beispielsweise zehn Versionen später überhaupt noch benötigt wird.

  • Wir werden uns gleich noch genauer mit Bibliotheken beschäftigen.

  • Drittens: Sie können die Auswirkungen der Optimierung beurteilen.

  • Bisher war es nicht einfach, festzustellen,ob eine Regel zwei oder 2.000 Klassen beibehält.

  • Sehen wir uns an, wie R8 im Kontext meiner App und der Bibliothek funktioniert, die wir uns vorhin angesehen haben.

  • Hier habe ich ein App-Modul und drei Bibliotheken.

  • Achten Sie besonders auf die dritte Bibliothek auf der rechten Seite.

  • Das ist die Analytics Bloat-Bibliothek, die eine paketweite Beibehaltungsregel enthält.

  • Zuerst identifiziert R8 den Stamm-Einstiegspunkt aus allen zusammengeführten Beibehaltungsregeln, die hier gelb hervorgehoben sind.

  • Im Allgemeinen sollten dies die Haupteinstiegspunkte der App sein, z. B. Klassen im Manifest.

  • Aufgrund der Platzhalter-Keep-Regel der Analytics Bloat-Bibliothek wird jede einzelne Klasse in der Bibliothek als Einstiegspunkt behandelt.

  • Als Nächstes verfolgt R8 die Erreichbarkeit im Aufrufdiagramm von diesen Wurzeln aus.

  • Alle Klassen, die von einem Stamm aus erreichbar sind, werden grün markiert, um sie beizubehalten. Nicht referenzierte Klassen werden als „toter Code“ eingestuft und hier rot markiert.

  • R8 führt dann Tree-Shaking durch und entfernt den nicht referenzierten ungenutzten Code.

  • Nach diesem Tree-Shaking oder dem ersten Verkleinerungsdurchlauf bleibt Folgendes übrig.

  • Unsere App hat einen Schrumpfungswert von 42,5 %. Das liegt zwar über dem Grenzwert von 25 % von Google Play, ist aber immer noch nicht so, wie wir es uns wünschen.

  • Das liegt hauptsächlich an der paketweiten Beibehaltungsregel aus der Analytics-Bibliothek.

  • Dadurch können nur 17 der verbleibenden 40 Klassen weiter verkleinert werden.

  • Ich möchte hier betonen, dass eine einzige fehlerhafte Keep-Regel die Optimierung Ihrer App erheblich beeinträchtigen kann.

  • Sehen wir uns vor diesem Hintergrund kurz die Bibliotheken an.

  • Im vergangenen Jahr hat unser Team viel Zeit und Mühe investiert, um zu verstehen, wie sich die Aufbewahrungsregeln für Bibliotheksnutzer auf die Optimierung des Ökosystems auswirken.

  • Viele Bibliotheken werden im Namen der Stabilität mit zu weit gefassten Regeln ausgeliefert.

  • Oder sie geben in ihrer Dokumentation allgemeine Regeln an, die App-Entwickler selbst in ihre App einfügen müssen.

  • Die Veröffentlichung mit allgemeinen Regeln ist jedoch nicht für die Stabilität erforderlich und wirkt sich negativ auf die Leistung aus. Die Kosten dafür tragen in erster Linie Sie als App-Entwickler.

  • Wir haben Fälle gesehen, in denen die gesamte Bibliothek durch eine paketweite Beibehaltungsregel wie die Analytics-Bloat-Bibliothek beibehalten wird. Im Fall dieser Bibliothek hatte dies weitreichende Auswirkungen auf das Optimierungspotenzial meiner App.

  • Ich möchte auch erwähnen, dass selbst unsere eigenen SDKs bei Google nicht davor gefeit waren und im vergangenen Jahr mehr als ein Dutzend Google-Bibliotheken zu weit gefasste Beibehaltungsregeln korrigiert haben.

  • Wenn Sie den Konfigurationsanalysator verwenden möchten, ist er direkt in AGP 9.3 integriert. Er ist aber auch für ältere Versionen verfügbar, wenn Sie Ihre R8-Version aktualisieren.

  • Wenn Sie diesen Befehl ausführen, wird der HTML-Bericht generiert, den wir gezeigt haben. Er enthält Ihre Beibehaltungsregeln, die Auswirkungen auf die Verkleinerung, Optimierung und Verschleierung für einzelne Regeln und Ihre App insgesamt.

  • Wenn Sie einen vollständigen R8-Release-Build ausführen, wird der Bericht automatisch generiert und ist unter diesem Dateipfad zu finden.

  • Der Vorteil dieser eigenständigen Aufgabe besteht darin, dass sie weniger Zeit in Anspruch nimmt als der vollständige Build.

Fallstudie: WHOOP-Optimierung

  • Das Team von WHOOP, das vielleicht sogar hier ist, hat den Konfigurationsanalysator ausgeführt und 74 % Verkleinerung, 74 % Verschleierung und 0 % Optimierung erhalten.

  • Wie Sie sich vorstellen können, war das Team von diesem Optimierungswert wirklich überrascht.

  • Dank der Sichtbarkeit durch den Konfigurationsanalysator konnten sie feststellen, dass ihr Build eine veraltete ProGuard-Datei verwendete, die die Optimierung deaktivierte. Durch den Wechsel zur aktualisierten Version konnten sie die Optimierung für ihre App aktivieren.

  • Dies führte zu einer Optimierung von 55 % und einer Reduzierung der APK-Größe um 4 %.

  • In Kombination mit Baseline-Profilen konnten sie die Kaltstartzeiten um 38 % verbessern und die Anzahl übermäßiger eingefrorener Frames um 28 % reduzieren.

  • Der Bildschirm links zeigt die App ohne Baseline-Profile, der Bildschirm rechts mit. Wie Sie sehen, startet die App mit Baseline-Profilen schneller.

  • Lass uns das noch einmal abspielen.

  • Cool.

  • Über diesen Link gelangen Sie zum Leitfaden für den Konfigurationsanalysator. Die manuelle Überprüfung des Berichts ist zwar eine gute Sache, kann aber je nach Status der R8-Konfiguration Ihrer App sehr zeitaufwendig sein.

  • Um die Sache zu beschleunigen, gibt es einen R8 Analyzer-Skill.

    $ android skills list
    $ android skills add r8-analyzer
  • Sie können den KI-Agenten bitten, Ihre R8-Konfiguration zu analysieren. Der Skill generiert dann nicht nur den Bericht des R8 Configuration Analyzer, sondern weist Sie auch auf die wichtigsten Regeln hin.

  • Hier ist eine Beispielausgabe. Ein paar Details: Der Optimierungsfaktor beträgt 42 %. Das bedeutet, dass 58 % der Codebasis nicht von R8 optimiert werden können.

  • Wenn wir uns den Abschnitt mit den Beibehaltungsregeln genauer ansehen, stellen wir fest, dass die paketweite Beibehaltungsregel für die Analytics-Bloat-Bibliothek verhindert, dass 22 % der Codebasis optimiert werden. Der Skill empfiehlt außerdem, diese Beibehaltungsregel zu verfeinern, da sie, wie wir bereits wissen, zu allgemein ist.

  • Mit dem R8-Konfigurationsanalysetool und dem Skill können Sie jetzt die Ursachen für App-Bloat und die Blockierung der DEX-Optimierung diagnostizieren.

Herausforderungen bei der Reflexion in der DEX-Optimierung

  • Aber wie können Sie eine zu weit gefasste „keep“-Regel sicher ersetzen, ohne versehentlich einen unerwarteten Absturz zu verursachen?

  • Ich übergebe an Chris, der Ihnen zeigt, wie Sie von alten Beibehaltungsregeln zu den neuen typsicheren, automatisch generierten Beibehaltungsannotationen wechseln.

  • Danke, Breana.

  • Sprechen wir darüber, wie wir hierher gekommen sind.

  • Bei dieser aufgeblähten Analysebibliothek mit all diesen Keep-Regeln, die im Grunde alles beibehalten, ist das Kernproblem die Reflexion.

  • Sehen wir uns also zur Erinnerung ein wenig reflektierenden Code an.

    fun createWithReflection(className: String): MyObject {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as MyObject
    }
  • Dies ist eine Funktion zur Objektinstanziierung, die ein bestimmtes Objekt aufbläht und dies reflektiv tut.

  • Der Nachteil im Zusammenhang mit der DEX-Optimierung ist, dass alle diese statischen Aufrufe bis zur Laufzeit verzögert werden.

  • Wenn wir uns nur den Code ansehen, kann unser R8-Optimierer je nachdem, woher dieser Klassenname stammt, nicht erkennen, was hier passiert.

  • In diesen Situationen wird die Dex-Optimierung standardmäßig unterbrochen. Daher müssen Sie normalerweise eine Keep-Regel hinzufügen.

  • Idealerweise vermeiden Sie das einfach, oder?

  • Idealerweise vermeiden wir Reflexionen vollständig, insbesondere jetzt im Jahr 2026, wo wir hervorragende Codegenerierungstools für die Serialisierung und die Abhängigkeitsinjektion haben, die diese Art von Problem vollständig beseitigen können.

  • Aber das ist nicht alles, oder?

  • In bestimmten Fällen ist Reflexion immer noch erforderlich.

  • Sehen wir uns also an, wie genau das in dieser aufgeblähten Bibliothek passiert ist.

  • Hier ist ein Beispiel für eine häufige Abhängigkeit, die optional ist.

    // 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
    }
  • Hier wird ein bestimmter Typ aufgebläht, auf den im Klassenpfad der Bibliothek nicht zugegriffen werden kann. Daher müssen wir einen String verwenden.

  • Wir verweisen auf diese bestimmte Klasse, die zum Zeitpunkt der Erstellung der Bibliothek möglicherweise vorhanden war oder nicht. Es ist also sehr wahrscheinlich, dass diese Bibliothek Reflexion verwendet, um ein Mitglied in einer zugehörigen Bibliothek zu finden.

  • Ein weiterer Fall ist die dynamische Instanziierung von Unterklassen.

  • Besonders wenn es sich um etwas aus einer Manifestdatei oder etwas handelt, das von der Serverseite gesendet wird, informieren wir die lokale App: „Hey, bitte erstelle dieses Ding später.“

  • Und die dritte Kategorie, die wir hier sehen, ist peinlicherweise die Serialisierung.

  • Ich habe eine JSON-Serialisierung, die durch Iteration aller Mitglieder erfolgt.

  • Das ist absolut ekelhaft, aber so etwas bleibt manchmal hängen.

  • Nicht immer wird es sofort repariert, wenn es nötig ist.

  • Aber was ist das Ergebnis?

  • Wenn sich diese reflektierenden Aufrufe häufen, werden Sie eher dazu motiviert, den einfachen Weg zu gehen und die Entwicklererfahrung statt der Nutzererfahrung zu optimieren.

  • Das ist natürlich nicht so toll. Also füge ich eine Aufgabe hinzu und alles ist gut.

    # TODO: clean this up
    -keep com.example.analytics.bloat.** { * };
  • Irgendwann werde ich es schon reparieren, oder?

  • Sprechen wir also darüber, was wir tun sollen, was wir zumindest vor heute tun sollten, wie Sie hier eine ideale Aufbewahrungsregel schreiben würden.

    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
  • Okay, schnall dich an.

  • Im ersten Teil möchten wir die bestimmte Klasse beibehalten, die von dieser reflektiven Funktion zurückgegeben wird.

    ### Keep the EventFilter
    -keep class com.example.analytics.bloat.EventFilter {
        *
    }
  • Nein, warte.

  • Wisst ihr was?

  • Das ist nicht richtig.

  • Das ist eine Kotlin-Klasse. Ich muss dafür sorgen, dass sie String-typisiert ist. Ich füge hier KT ein.

    ### Keep the actual EventFilter
    -keep class com.example.analytics.bloat.EventFilterKt {
        *
    }
  • Tatsächlich gebe ich hier nicht EventFilter zurück, sondern eine Unterklasse von EventFilter. Ich muss also Klassen beibehalten, die EventFilter erweitern, um dies korrekt aufblähen zu können.

    ### Keep the EventFilter subclasses
    -keep class ** extends com.example.analytics.bloat.EventFilterKt {
        *
    }
  • Und wenn ich das effizienter machen möchte, was sollte ich als guter Bibliotheksentwickler noch tun?

  • Ich sollte -keepclasseswithmembers sagen und nur die Klassen behalten, die diesen bestimmten <init>-Konstruktor haben, den ich zum Aufblähen des Objekts verwende.

    ### Keep the EventFilter subclasses, if they have this constructor
    -keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
       void <init>();
    }
  • Und wenn ich wirklich nett sein möchte, füge ich allowaccessmodification hinzu, um weitere Optimierungen für alle APIs zu ermöglichen, die von dieser Keep-Regel betroffen sind. Ich möchte ja nicht verhindern, dass R8 weitere Optimierungen vornimmt, sondern ich möchte ein guter Bürger sein.

    ### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
    -keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
       void <init>();
    }
  • Und dann der letzte Schritt, den fast niemand macht, weil es zusätzliche Arbeit ist, der aber für eine Bibliothek großartig ist: die bedingte Ausführung.

    ### -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>();
    }
  • Konzeptionell müssen Sie nicht alle diese Unterklassen beibehalten, es sei denn, Sie rufen diese Funktion tatsächlich auf. Das ist die Aufgabe dieser if-Anweisung am Anfang einer Keep-Regel.

  • Ich könnte Sie bitten, die Hand zu heben, aber ich weiß, dass das fast niemand tut, oder?

  • Also, sie tatsächlich zu schreiben und bedingt zu machen.

  • Was wäre, wenn wir das noch ein bisschen besser machen könnten?

  • Sehen wir uns zuerst an, was wir haben.

  • Vielleicht gibt es ja eine andere Möglichkeit.

  • Wir haben darüber gesprochen, was passiert, wenn ich eine enge, manuell geschriebene Aufbewahrungsregel erstelle.

  • Technisch ist das zwar möglich, aber es ist schwer zu schreiben, schwer zu lesen und sehr schwer zu pflegen, weil es viele kleine String-Typen enthält, die leicht kaputtgehen, wenn etwas darunter passiert.

  • Darunter ändert sich etwas.

  • Und wir haben Alternativen.

  • Wir haben Annotationen wie @Keep.

  • @Keep ist schon lange im AndroidX-Annotationspaket enthalten, wird aber heutzutage nicht mehr empfohlen.

    @androidx.annotation.Keep
    class EventFilter { … }
    
    @androidx.annotation.Keep
    class SizeBasedEventFilter { … }
    
    @androidx.annotation.Keep
    class DropLogsEventFilter { … }
  • Für Bibliotheksentwickler ist das keine gute Lösung und wird intern sogar verboten, da es zwar einfacher zu verwalten, aber sehr unflexibel ist und einige der zuvor erwähnten Optimierungen nicht bietet.

  • Das ist nicht möglich.

  • Es ist nicht möglich, die Aufrufstelle zu analysieren.

  • Sehen wir uns den Fall von vorhin an: „Ich kann einfach @Keep in meine EventFilter einfügen, dann ist alles gut, oder?“

    @Keep
    class EventFilter { … }
  • Na ja, ähm, nein.

  • „@Keep“ ist nicht auf Unterklassen ausgerichtet, sondern nur auf diese Klasse.

    @Keep
    class EventFilter { … }
    
    @Keep
    class SizeBasedEventFilter { … }
    
    @Keep
    class DropLogsEventFilter { … }
  • Ich muss also alle Unterklassen einbeziehen und schreibe jetzt mehr @Keep-Annotationen, als ich mit der einen Keep-Regel, die schwer zu schreiben war, geschrieben hätte.

  • Beide haben also Probleme, oder?

  • Sie werden leicht vergessen, Sie erfahren erst spät davon und sie sind standardmäßig nicht vom Anrufer abhängig.

  • Besonders für Bibliotheken möchten wir eine bessere Lösung finden.

  • Wir möchten, dass es einfacher wird, Dinge direkt nach dem Auspacken richtig zu machen.

  • Eines der großen Probleme, die wir hier sehen, ist, dass diese Dinge weit entfernt von der Reflexion sind, vor der wir uns eigentlich schützen wollen.

  • Letztendlich wollen wir eine Lösung, die direkt am, sagen wir, „bösen“ Reflexionscode ansetzt, der die Zielobjekte aufbläht.

Einführung in Jetpack Keep

  • Sprechen wir also über Jetpack Keep, die neue Bibliothek und das neue Plug-in von AndroidX, die genau dieses Problem lösen sollen.

  • Das Ziel von Jetpack Keep ist es, zuerst Keep-Regeln aus Annotationen zu generieren, aber auch Annotationen direkt aus der Reflexion zu generieren, die bereits in Ihrer Codebasis vorhanden ist.

  • Die Einrichtung ist also sehr einfach.

    plugins {
       id("com.android.library")
       id("androidx.annotation.keep")
    }
    
    dependencies {
        implementation("androidx.annotation.keep:keep")
        ...
    }
  • Es ist ein Gradle-Plug-in und eine Annotationsbibliothek.

  • Sehen wir uns an, wie das zur Build-Zeit funktioniert.

  • Wenn ich also das vorherige Beispiel habe, in dem ich eine Unterklasse eines bekannten Typs aufblähe, was muss ich dann tun?

  • Ich führe einfach lintFix aus.

    ./gradlew analyticsbloat:lintFix
  • Lint analysiert hier die gesamte Reflexion, die innerhalb dieser Methode stattfindet, und sagt: „Hey, du brauchst diese Annotation.“

    @UsesReflectionToConstruct(
       classConstant = EventFilter::class,
       parameterTypes = []
    )
    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
  • Diese Annotation drückt genau aus, was die Reflexion innerhalb dieses Aufrufs bewirkt.

  • Sie besagt: „Ich instanziiere eine Klasse dieses Typs.“

  • In diesem Fall ist EventFilter eine bekannte Klasse.

  • Ich verwende keinen String, um darauf zu verweisen, weil das nicht nötig ist.

  • Das ist zusätzliche Sicherheit und ich codiere sogar die parameterTypes, damit genau angegeben wird, welcher Konstruktor beibehalten wird.

  • Aber was bewirkt das?

  • Wie wirkt sich das auf einen eventuellen R8-Build aus?

  • Es wird die Beibehaltungsregel generiert. Wie Sie sehen, ist dies genau dieselbe ideale Beibehaltungsregel, die wir zuvor hatten.

    # 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>();
    }
  • Es zielt auf genau dieselben Klassen und Unterklassen ab und ist von diesem Reflexionspunkt abhängig.

  • Und ich muss mich nicht um die Annotation aller Unterklassen kümmern.

  • Ich muss keine separate Datei mit Aufbewahrungsregeln an anderer Stelle speichern.

  • So lassen sich Ihre Aufbewahrungsregeln viel einfacher verwalten.

  • Sehen wir uns das Beispiel mit der optionalen Abhängigkeit noch einmal an.

    @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()
    }
  • Die Klasse war nicht im Klassenpfad, aber ich kann trotzdem darauf verweisen.

  • Ich verwende dazu einfach einen String, aber alles andere, die Parametertypen, kann über Klassenreferenzen referenziert werden, also mache ich das.

  • Auch hier wird die ideale Aufbewahrungsregel für diesen Fall generiert.

    # 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);
    }
  • Sie ist bedingt, spezifisch und zielgerichtet und ermöglicht Optimierungen, wo immer dies für diese Zielklassen möglich ist.

  • Hier ist die Reflexion über Felder. In diesem speziellen Fall habe ich alle Felder innerhalb eines Objekts iteriert. Aber auch hier können wir das einfach mit dem Sternchenoperator erledigen, der alle Felder anspricht.

    // 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
    }
  • So kann ich alle Felder als Ziel festlegen und es wird genau die Keep-Regel generiert, die man für diese unglückliche Reflexion erwarten würde, die nicht in meiner App sein sollte. Aber ich werde das später bereinigen, versprochen.

    # 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>;
    }
  • Was macht Jetpack Keep also?

  • Der Wartungsaufwand für Aufbewahrungsregeln wird drastisch reduziert.

  • Alles, was Sie automatisch generieren können, wird aus Ihrem Zuständigkeits- und Risikobereich für die Einhaltung von Aufbewahrungsregeln entfernt.

  • Außerdem erhalten Sie den Determinismus von Lint.

  • Es werden nicht nur Probleme behoben, während Sie iterieren und migrieren, sondern es werden auch neue Reflexionen und reflektierende Änderungen erkannt, wenn sich der Code ändert. Diese Sicherheit ist absolut entscheidend.

  • Kann ich das verwenden?

  • Welche Abhängigkeiten gibt es hier?

  • Für das Plug-in und Lint ist AGP 8.7 erforderlich.

  • Das sollte fast jeder sein.

  • Sie sind aber auch sehr flexibel, was die Verwendung angeht.

  • Sie können diese Funktion in Apps verwenden.

  • Sie können sie in Android-Bibliotheken, AARs, KMP-Bibliotheken und JARs verwenden – in diesem Fall ist ein wenig manuelle Verknüpfung erforderlich.

  • Das wichtigste Merkmal ist jedoch, dass diese Funktion für Bibliotheksentwickler sicher ist.

  • Selbst wenn jemand eine uralte Version von R8 oder sogar ein Drittanbieter-Tool verwendet, das Ihre Bibliothek nutzt, exportieren wir immer noch die tatsächlichen Standard-Consumer-Regeln, damit jeder Sie weiterhin verwenden kann und sich keine Sorgen machen muss, dass er ein neues inkompatibles Format verwendet.

Apps mit der Android-Befehlszeile analysieren

  • Nachdem ich nun darüber gesprochen habe, wie Jetpack Keep Ihnen bei der Optimierung helfen und Ihnen die Sorge um den Schutz Ihrer reflektierenden Codeaufrufe nehmen kann, möchte ich über die neuen Funktionen sprechen, die wir vor Kurzem in der Android-Befehlszeile eingeführt haben, um die letzte Release-Phase noch einmal zu überprüfen.

  • Breana hat vorhin erwähnt, dass es Tools gibt, mit denen Sie Informationen zu Ihrer Größe, Ihrer DEX-Größe und Ihren Optimierungsprozentsätzen erhalten können.

  • Inzwischen haben wir damit begonnen, diese Funktion auch über die Android-Befehlszeile verfügbar zu machen.

  • Dabei wird genau dieselbe Logik wie im Google Play Store verwendet. Sie müssen also kein Bundle in den Google Play Store hochladen, sondern können es einfach über die Befehlszeile analysieren lassen.

    $ android analyze <mybundle.aab>
    $ android analyze <mybundle.apk>
  • Und Sie erhalten genau dieselben Informationen direkt und sofort mit der lokalen Analyse.

  • In diesem Fall wird für die App eine Warnung angezeigt, dass die Optimierungsrate sehr niedrig ist. Außerdem wird die DEX-Größe und der Optimierungsprozentsatz angezeigt, genau wie im Google Play Store.

  • Außerdem gibt es eine JSON-Ausgabe. Wenn Sie also ein Skript schreiben möchten, um Regressionstests zu automatisieren, falls einer dieser Werte drastisch sinkt, können Sie das mit diesen JSON-Berichten ganz einfach tun. Denn wie gesagt, das sind die Zahlen, die Google Play erzwingt, und Sie sollten sie im Auge behalten.

    {
      "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"
      }
  • Hier sehen wir genau dieselben Zahlen.

  • Das ist aber nicht die einzige Funktion von android analyze.

  • android analyze kann auch für Bibliotheken ausgeführt werden.

    $ android analyze <library.aar>
    $ android analyze <library.jar>
  • Und wie Breana bereits erwähnt hat, gibt es viele Probleme mit Bibliotheken, die mit unglücklichen Keep-Regeln ausgeliefert werden.

  • Sehen wir uns also einige dieser Probleme an.

  • Wenn ich das also für eine bestimmte AAR-Datei ausführe, sehe ich, dass diese Bibliothek eine Reihe von Problemen hat, die ganz oben hervorgehoben sind.

  • Diese Bibliothek hat ein hohes Beibehaltungsverhältnis. Das bedeutet, dass sie aufgrund von unbegrenzten Beibehaltungsregeln zu viele Elemente beibehält.

  • Sie enthält allgemeine Beibehaltungsregeln, die verhindern, dass sie optimiert wird. Unten sehen Sie einige Zahlen, die genau zeigen, wie schlecht das ist. Außerdem enthält sie Beibehaltungsregeln, die, was vielleicht am beängstigendsten ist, auf nichts abzielen, was wahrscheinlich bedeutet, dass sie auf jemand anderen in einer unbegrenzten Weise abzielen.

  • Sehr gefährliche Code-Smells.

  • All das können Sie in einer AAR-Datei prüfen. Vor allem als Bibliotheksherausgeber ist das eine gute Möglichkeit, um sicherzustellen, dass Sie keine fehlerhaften Beibehaltungsregeln ausliefern und dass diese nicht durchsickern, wenn jemand an der Bibliothek arbeitet, der vielleicht nicht so viel Erfahrung mit dem Schreiben von Beibehaltungsregeln hat.

  • Hier sehen Sie, dass wir auch eine Beibehaltungsquote ausgeben.

  • So können Sie sich die Gesamtqualität Ihrer Mediathek vorstellen.

  • Wenn ich eine einzelne Bibliothek komplett eigenständig, ohne Verweise, mit R8 optimiere, sollte sie null Byte groß sein, oder?

  • Sie sollte im Grunde leer sein, da nichts darin aufbewahrt wird.

  • Das wäre ideal.

  • In diesem Fall ist das nicht so.

  • Sie können die Größe von R8 und D8 direkt sehen.

  • Wir heben auch einige der schlechtesten Aufbewahrungsregeln hervor, um sie für Sie etwas sichtbarer zu machen.

  • Auch diese Datei hat ein JSON-Format. Egal, ob Sie als App-Entwickler Bibliotheksupdates einschränken oder als Bibliotheksentwickler große Qualitätseinbußen in Ihrer Bibliothek verhindern möchten, Sie können dieselbe Analyse durchführen und diese Probleme mit einem einfachen Skript verhindern, das die Android-Befehlszeile umschließt.

    {
      "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.** { *; }",
                  },
                  ...
                ]
              },
              ...
    }
  • Wenn Sie also ein Problem mit einer Bibliothek finden, versuchen Sie zuerst, sie zu aktualisieren.

  • Wie Breana bereits erwähnt hat, haben wir mehr als ein Dutzend Google-Bibliotheken korrigiert, um einige sehr unglückliche Keep-Regeln zu beheben. Es gibt auch andere Bibliotheken, die korrigiert wurden, aber sie werden von den Apps, die sie verwenden, nicht aktualisiert.

  • Das ist ein toller erster Schritt.

  • Zweitens können Sie nach einem Problem suchen und sich an den Entwickler der Bibliothek wenden. Möglicherweise können Sie einen Analysebericht anhängen, der ihm zeigt: „Hey, du behältst zu viel.

  • Bitte helfen Sie mir, meine App zu optimieren.“

Wichtige Erkenntnisse und Ressourcen

  • Fassen wir noch einmal zusammen: Wir haben über den R8 Config Analyzer gesprochen, mit dem Sie sich alles ansehen können, was in Ihrem R8-Build passiert, und der Ihnen zeigt, was optimiert werden kann und was nicht.

  • Wir haben über Jetpack Keep gesprochen, die neue Bibliothek, mit der Sie automatisch Keep-Regeln schreiben können, ohne einen Finger rühren zu müssen.

  • Und Android CLI, das neue Tool, mit dem Sie den letzten Schritt noch einmal überprüfen können, um sicherzustellen, dass Ihr Agent, ein Junior-Entwickler in Ihrem Team, nichts für Ihre Nutzer ruiniert hat.

  • Wenn Sie mehr erfahren möchten, finden Sie Dokumentationen zu R8, zum Konfigurationsanalysator und zu Jetpack Keep.

  • In der Dokumentation zur Android-Befehlszeile wird der Befehl „analyze“ und seine Verwendung beschrieben.

  • Vielen Dank.

Code

Code-Snippets

Kotlin-Skript-Snippet bei 05:15 in „Einführung in die DEX-Optimierung“
android {
    buildTypes {
        release {
            optimization {
                // no need to specify default
                // rules and resource shrinking
                enabled = true
            }
        }
    }
    ...
}
Kotlin-Skript-Snippet bei 05:15 in „Einführung in die DEX-Optimierung“
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
            )
            ...
        }
    }
    ...
}
Shell-Snippet bei 13:19 in „Fallstudie: WHOOP-Optimierung“
$ android skills list
$ android skills add r8-analyzer
Kotlin-Snippet bei 14:41 in „Challenges of reflection in DEX optimization“
fun createWithReflection(className: String): MyObject {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as MyObject
}
Kotlin-Snippet bei 15:40 in „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
}
Shell-Snippet bei 16:52 in „Challenges of reflection in DEX optimization“
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };
Kotlin-Snippet bei 16:59 in „Challenges of reflection in DEX optimization“
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
Shell-Snippet bei 17:07 in „Challenges of reflection in DEX optimization“
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
    *
}
Shell-Snippet bei 17:15 in „Challenges of reflection in DEX optimization“
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
    *
}
Shell-Snippet bei 17:19 in „Challenges of reflection in DEX optimization“
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
    *
}
Shell-Snippet bei 17:40 in „Challenges of reflection in DEX optimization“
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
Shell-Snippet bei 17:50 in „Challenges of reflection in DEX optimization“
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
Shell-Snippet bei 18:03 in „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>();
}
Kotlin-Snippet bei 18:57 in „Challenges of reflection in DEX optimization“
@androidx.annotation.Keep
class EventFilter { … }

@androidx.annotation.Keep
class SizeBasedEventFilter { … }

@androidx.annotation.Keep
class DropLogsEventFilter { … }
Kotlin-Snippet bei 19:22 in „Challenges of reflection in DEX optimization“
@Keep
class EventFilter { … }
Kotlin-Snippet bei 19:29 in „Challenges of reflection in DEX optimization“
@Keep
class EventFilter { … }

@Keep
class SizeBasedEventFilter { … }

@Keep
class DropLogsEventFilter { … }
Kotlin-Skript-Snippet bei 20:38 in „Einführung in Jetpack Keep“
plugins {
   id("com.android.library")
   id("androidx.annotation.keep")
}

dependencies {
    implementation("androidx.annotation.keep:keep")
    ...
}
Shell-Snippet bei 20:54 in „Einführung in Jetpack Keep“
./gradlew analyticsbloat:lintFix
Kotlin-Snippet bei 20:55 in „Einführung in Jetpack Keep“
@UsesReflectionToConstruct(
   classConstant = EventFilter::class,
   parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
Shell-Snippet bei 21:25 in „Einführung in 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>();
}
Kotlin-Snippet bei 21:52 in „Einführung in 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()
}
Shell-Snippet bei 22:05 in „Einführung in 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);
}
Kotlin-Snippet bei 22:16 in „Einführung in 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
}
Shell-Snippet bei 22:27 in „Einführung in 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 bei 24:22 in „Analyzing apps with Android CLI“
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>
JSON-Snippet bei 24:52 in „Analyzing apps with Android CLI“
{
  "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 bei 25:17 in „Analyzing apps with Android CLI“
$ android analyze <library.aar>
$ android analyze <library.jar>
JSON-Snippet bei 26:46 in „Analyzing apps with Android CLI“
{
  "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.** { *; }",
              },
              ...
            ]
          },
          ...
}