Articles

Praktische Beispiele für ProGuard-Regeln

Wojtek Kaliciński
Wojtek Kaliciński

Folgen Sie

Februar 20, 2018 · 9 min read

In meinem vorherigen Artikel habe ich erklärt, warum jeder ProGuard für seine Android-Apps verwenden sollte, wie man es aktiviert und welche Art von Fehlern dabei auftreten können. Es gab viel Theorie, da ich denke, dass es wichtig ist, die zugrunde liegenden Prinzipien zu verstehen, um auf mögliche Probleme vorbereitet zu sein.

Ich habe auch in einem separaten Artikel über das sehr spezifische Problem der Konfiguration von ProGuard für einen sofortigen App-Build gesprochen.

In diesem Teil möchte ich über die praktischen Beispiele von ProGuard-Regeln für eine mittelgroße Beispiel-App sprechen: Plaid von Nick Butcher.Plaid erwies sich tatsächlich als ein großartiges Thema für die Erforschung von ProGuard-Problemen, da es eine Mischung aus 3rd-Party-Bibliotheken enthält, die Dinge wie Annotation Processing und Code Generation, Reflection, Java Resource Loading und Native Code (JNI) verwenden. Ich habe einige praktische Ratschläge extrahiert und notiert, die im Allgemeinen für andere Apps gelten sollten:

Datenklassen

public class User {
String name;
int age;
...
}

Wahrscheinlich hat jede App eine Art Datenklasse (auch bekannt als DMOs, Modelle usw. b. je nach Kontext und Position in der Architektur Ihrer App). Die Sache mit Datenobjekten ist, dass sie normalerweise irgendwann in ein anderes Medium geladen oder gespeichert (serialisiert) werden, z. B. in ein Netzwerk (eine HTTP-Anforderung), eine Datenbank (über ein ORM), eine JSON-Datei auf der Festplatte oder in einem Firebase-Datenspeicher.

Viele der Tools, die die Serialisierung und Deserialisierung dieser Felder vereinfachen, basieren auf Reflektion. GSON, Retrofit, Firebase – sie alle überprüfen Feldnamen in Datenklassen und wandeln sie in eine andere Darstellung um (zum Beispiel: {"name”: "Sue”, "age”: 28} ), entweder für den Transport oder die Speicherung. Das gleiche passiert, wenn sie Daten in ein Java-Objekt lesen – sie sehen ein Schlüssel-Wert-Paar "name”:”John” und versuchen, es auf ein Java-Objekt anzuwenden, indem sie ein String name Feld .

Fazit: Wir können ProGuard keine Felder in diesen Datenklassen umbenennen oder entfernen lassen, da sie dem serialisierten Format entsprechen müssen. Es ist eine sichere Sache, eine @Keep Anmerkung für die gesamte Klasse oder eine Platzhalterregel für alle Ihre Modelle hinzuzufügen:

-keep class io.plaidapp.data.api.dribbble.model.** { *; }

Warnung: Es ist möglich, beim Testen einen Fehler zu machen, wenn Ihre App für dieses Problem anfällig ist. Wenn Sie beispielsweise ein Objekt in JSON serialisieren und es in Version N Ihrer App ohne die richtigen Aufbewahrungsregeln auf der Festplatte speichern, sehen die gespeicherten Daten möglicherweise folgendermaßen aus: {"a”: "Sue”, "b”: 28} . Da ProGuard Ihre Felder in a und b umbenannt hat, scheint alles zu funktionieren, die Daten werden korrekt gespeichert und geladen.

Wenn Sie Ihre App jedoch erneut erstellen und Version N+1 Ihrer App veröffentlichen, beschließt ProGuard möglicherweise, Ihre Felder in etwas anderes umzubenennen, z. B. c und d. Infolgedessen können zuvor gespeicherte Daten nicht geladen werden.

Sie müssen sicherstellen, dass Sie die richtigen Keep-Regeln an erster Stelle haben.

Java-Code, der von der nativen Seite (JNI) aufgerufen wird

Die Standard-ProGuard-Dateien von Android (Sie sollten sie immer einschließen, sie haben einige wirklich nützliche Regeln) enthalten bereits eine Regel für Methoden, die auf der nativen Seite implementiert sind (-keepclasseswithmembernames class * { native <methods>; }). Leider gibt es keine Möglichkeit, Code in die entgegengesetzte Richtung aufzurufen: von JNI nach Java.

Mit JNI ist es durchaus möglich, ein JVM-Objekt zu konstruieren oder eine Methode für ein JVM-Handle aus C / C ++ – Code zu finden und aufzurufen.

Fazit: Da ProGuard nur Java-Klassen untersuchen kann, kennt es keine Verwendungen, die in nativem Code vorkommen. Wir müssen solche Verwendungen von Klassen und Mitgliedern explizit über eine @Keep Annotation oder -keep Regel beibehalten.

-keep, includedescriptorclasses 
class in.uncod.android.bypass.Document { *; }
-keep, includedescriptorclasses
class in.uncod.android.bypass.Element { *; }

Öffnen von Ressourcen aus JAR / APK

Android verfügt über ein eigenes Ressourcen- und Asset-System, das für ProGuard normalerweise kein Problem darstellen sollte. In einfachem Java gibt es jedoch einen anderen Mechanismus zum Laden von Ressourcen direkt aus einer JAR-Datei, und einige Bibliotheken von Drittanbietern verwenden ihn möglicherweise, selbst wenn er in Android-Apps kompiliert wurde (in diesem Fall versuchen sie, von der APK zu laden).

Das Problem ist, dass diese Klassen normalerweise nach Ressourcen unter ihrem eigenen Paketnamen suchen (was in einen Dateipfad im JAR oder APK übersetzt wird). Nach der Kompilierung kann es daher vorkommen, dass sich die Klasse und ihre Ressourcendatei in der endgültigen APK nicht mehr im selben Paket befinden.

Um ladende Ressourcen auf diese Weise zu identifizieren, können Sie nach Aufrufen von Class.getResourceAsStream / getResource und ClassLoader.getResourceAsStream / getResource in Ihrem Code und in allen Bibliotheken von Drittanbietern suchen, auf die Sie angewiesen sind.

Fazit: Wir sollten den Namen jeder Klasse behalten, die Ressourcen aus der APK mit diesem Mechanismus lädt.

Tatsächlich gibt es zwei — eine in der OkHttp-Bibliothek und eine in Jsoup:

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-keepnames class org.jsoup.nodes.Entities

So erstellen Sie Regeln für Bibliotheken von Drittanbietern

In einer idealen Welt würde jede Abhängigkeit, die Sie verwenden, die erforderlichen ProGuard-Regeln in der AAR bereitstellen. Manchmal vergessen sie dies oder veröffentlichen nur JARs, die keine standardisierte Möglichkeit haben, ProGuard-Regeln bereitzustellen.

Denken Sie in diesem Fall daran, die Dokumentation zu lesen, bevor Sie mit dem Debuggen Ihrer App beginnen und Regeln erstellen. Einige Bibliotheksautoren geben empfohlene ProGuard-Regeln an (z. B. Retrofit in Plaid), mit denen Sie viel Zeit und Frustration sparen können. Leider tun dies viele Bibliotheken nicht (wie bei Jsoup und Bypass, die in diesem Artikel erwähnt werden). Beachten Sie auch, dass die mit der Bibliothek gelieferte Konfiguration in einigen Fällen nur mit deaktivierten Optimierungen funktioniert.

Also, wie kommt man mit Regeln, wenn die Bibliothek sie nicht liefert?
Ich kann Ihnen nur einige Hinweise geben:

  1. Lesen Sie die Build-Ausgabe und logcat!
  2. Build—Warnungen werden Ihnen sagen, welche -dontwarn Regeln hinzuzufügen
  3. ClassNotFoundExceptionMethodNotFoundException und FieldNotFoundException wird Ihnen sagen, welche -keep Regeln zum Hinzufügen

Sie sollten froh sein, wenn Ihre App mit aktiviertem ProGuard abstürzt – Sie haben einen Ort, an dem Sie Ihre Untersuchung beginnen können 🙂

Die schlimmsten Probleme beim Debuggen sind, wenn Ihre App funktioniert, aber beispielsweise keinen Bildschirm anzeigt oder keine Daten aus dem Netzwerk lädt.

Hier müssen Sie einige der in diesem Artikel beschriebenen Szenarien berücksichtigen und sich die Hände schmutzig machen, sogar in den Code von Drittanbietern eintauchen und verstehen, warum er möglicherweise fehlschlägt, z. B. wenn er Reflection, Introspection oder JNI verwendet.

Debugging und Stack-Traces

ProGuard entfernt standardmäßig viele Codeattribute und versteckte Metadaten, die für die Programmausführung nicht erforderlich sind . Einige davon sind für den Entwickler tatsächlich nützlich — zum Beispiel möchten Sie möglicherweise die Quelldateinamen und Zeilennummern für Stack-Traces beibehalten, um das Debuggen zu vereinfachen:

-keepattributes SourceFile, LineNumberTable

Sie sollten auch daran denken, die ProGuard-Zuordnungsdateien zu speichern, die beim Erstellen einer Release-Version erstellt wurden, und sie in Play hochzuladen, um Stack-Traces von Abstürzen Ihrer Benutzer zu entfernen.

Wenn Sie einen Debugger anhängen, um den Methodencode in einem überwachten Build Ihrer App zu durchlaufen, sollten Sie auch die folgenden Attribute beibehalten, um einige Debug-Informationen über lokale Variablen beizubehalten (Sie benötigen nur diese Zeile in Ihrem debug Build-Typ):

-keepattributes LocalVariableTable, LocalVariableTypeTable

Minimierter Debug-Build-Typ

Die Standard-Build-Typen sind so konfiguriert, dass Debug ProGuard nicht ausführt. Das ist sinnvoll, da wir bei der Entwicklung schnell iterieren und kompilieren möchten, aber dennoch möchten, dass der Release-Build mit ProGuard so klein und optimiert wie möglich ist.

Aber um ProGuard-Probleme vollständig zu testen und zu debuggen, ist es gut, einen separaten, minimierten Debug-Build wie diesen einzurichten:

buildTypes {
debugMini {
initWith debug
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
matchingFallbacks =
}
}

Mit diesem Build-Typ können Sie den Debugger verbinden, UI-Tests (auch auf einem CI-Server) ausführen oder Ihre App auf mögliche Probleme in einem Build testen, der Ihrem Release-Build so nahe wie möglich kommt.

Fazit: Wenn Sie ProGuard verwenden, sollten Sie Ihre Release-Builds immer gründlich überprüfen, indem Sie entweder End-to-End-Tests durchführen oder manuell alle Bildschirme in Ihrer App durchgehen, um festzustellen, ob etwas fehlt oder abstürzt.

Runtime annotations, type introspection

ProGuard entfernt standardmäßig alle Annotationen und sogar einige überschüssige Typinformationen aus Ihrem Code. Für einige Bibliotheken ist das kein Problem — diejenigen, die Anmerkungen verarbeiten und Code zur Kompilierungszeit generieren (wie Dagger 2 oder Glide und viele mehr), benötigen diese Anmerkungen möglicherweise später nicht, wenn das Programm ausgeführt wird.

Es gibt eine andere Klasse von Tools, die Annotationen tatsächlich überprüfen oder Typinformationen von Parametern und Ausnahmen zur Laufzeit anzeigen. Retrofit beispielsweise fängt Ihre Methodenaufrufe mithilfe eines Proxy -Objekts ab und betrachtet dann Anmerkungen und Typinformationen, um zu entscheiden, was aus der HTTP-Anforderung eingefügt oder gelesen werden soll.

Fazit: Manchmal ist es erforderlich, Typinformationen und Anmerkungen beizubehalten, die zur Laufzeit gelesen werden, im Gegensatz zur Kompilierungszeit. Sie können die Attributliste im ProGuard-Handbuch überprüfen.

-keepattributes *Annotation*, Signature, Exception

Wenn Sie die Standardkonfigurationsdatei von Android ProGuard (getDefaultProguardFile('proguard-android.txt')) verwenden, werden die ersten beiden Optionen — Anmerkungen und Signatur — für Sie angegeben. Wenn Sie die Standardeinstellung nicht verwenden, müssen Sie sicherstellen, dass Sie sie selbst hinzufügen (es schadet auch nicht, sie einfach zu duplizieren, wenn Sie wissen, dass sie für Ihre App erforderlich sind).

Alles in das Standardpaket verschieben

Die Option -repackageclasses wird in der ProGuard-Konfiguration standardmäßig nicht hinzugefügt. Wenn Sie Ihren Code bereits verschleiern und Probleme mit den richtigen Aufbewahrungsregeln behoben haben, können Sie diese Option hinzufügen, um die DEX-Größe weiter zu reduzieren. Es funktioniert, indem alle Klassen in das Standardpaket (root) verschoben werden, wodurch im Wesentlichen der von Zeichenfolgen wie „com.Beispiel.myapp.somepackage“.

-repackageclasses

ProGuard-Optimierungen

Wie bereits erwähnt, kann ProGuard 3 Dinge für Sie tun:

  1. Es entfernt nicht verwendeten Code,
  2. benennt Bezeichner um, um den Code zu verkleinern,
  3. führt ganze Programmoptimierungen durch.

So wie ich es sehe, sollte jeder versuchen, seinen Build so zu konfigurieren, dass er 1 erhält. und 2. arbeiten.

Zu entsperren 3. (zusätzliche Optimierungen), müssen Sie eine andere Standard-ProGuard-Konfigurationsdatei verwenden. Ändern Sie den Parameter proguard-android.txt in proguard-android-optimize.txt in Ihrem build.gradle:

release {
minifyEnabled true
proguardFiles
getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}

Dadurch wird Ihr Release-Build langsamer, Ihre App wird jedoch möglicherweise schneller ausgeführt und die Codegröße wird dank Optimierungen wie Methodeninlining noch weiter reduziert , Klassenzusammenführung und aggressiveres Entfernen von Code. Seien Sie jedoch darauf vorbereitet, dass es neue und schwer zu diagnostizierende Fehler einführen könnte, also verwenden Sie es mit Vorsicht und wenn etwas nicht funktioniert, deaktivieren Sie bestimmte Optimierungen oder deaktivieren Sie die Verwendung der Optimierungskonfiguration insgesamt.

Im Fall von Plaid störten ProGuard-Optimierungen, wie Retrofit Proxy-Objekte ohne konkrete Implementierungen verwendet, und entfernten einige Methodenparameter, die tatsächlich erforderlich waren. Ich musste diese Zeile zu meiner Konfiguration hinzufügen:

-optimizations !method/removal/parameter

Eine Liste möglicher Optimierungen und deren Deaktivierung finden Sie im ProGuard-Handbuch.

Wann @Keep und -keep zu verwenden sind

@Keep Die Unterstützung ist tatsächlich als eine Reihe von -keep Regeln in der Standard-Android-ProGuard-Regeldatei implementiert, sodass sie im Wesentlichen gleichwertig sind. Die Angabe von -keep Regeln ist flexibler, da es Platzhalter bietet, Sie können auch verschiedene Varianten verwenden, die etwas andere Dinge tun (-keepnames-keepclasseswithmembers und mehr).

Wann immer eine einfache „keep this class“ – oder „keep this method“ -Regel benötigt wird, bevorzuge ich die Einfachheit des Hinzufügens einer@Keep Annotation auf der Klasse oder Mitglied, wie es in der Nähe des Codes bleibt, fast wie Dokumentation.

Wenn ein anderer Entwickler, der nach mir kommt, den Code umgestalten möchte, werden sie sofort wissen, dass eine mit @Keep gekennzeichnete Klasse / ein Mitglied eine spezielle Behandlung erfordert, ohne daran denken zu müssen, die ProGuard-Konfiguration zu konsultieren und etwas zu riskieren. Außerdem sollten die meisten Code-Refactorings in der IDE die Annotation @Keep mit der Klasse automatisch beibehalten.

Plaid stats

Hier sind einige Statistiken von Plaid, die zeigen, wie viel Code ich mit ProGuard entfernen konnte. Bei einer komplexeren App mit mehr Abhängigkeiten und einem größeren DEX können die Einsparungen noch beträchtlicher sein.