Articles

Praktische ProGuard regels voorbeelden

Wojtek Kaliciński
Wojtek Kaliciński

Volgen

Feb 20, 2018 · 9 min lezen

In mijn vorige artikel heb ik uitgelegd waarom moet iedereen gebruik ProGuard voor hun Android-apps, het inschakelen en wat voor soort fouten die je kunt tegenkomen bij het doen. Er was veel theorie bij betrokken, omdat ik denk dat het belangrijk is om de onderliggende principes te begrijpen om voorbereid te zijn op mogelijke problemen.

Ik sprak ook in een apart artikel over het zeer specifieke probleem van het configureren van ProGuard voor een Instant app build.

in dit deel wil ik het hebben over de praktische voorbeelden van ProGuard regels op een middelgrote sample app: Plaid van Nick Butcher.

Plaid bleek eigenlijk een geweldig onderwerp voor het onderzoeken van ProGuard problemen, omdat het een mix van 3rd party bibliotheken bevat die dingen gebruiken zoals annotatie verwerking en code generatie, reflectie, java resource loading en native code (JNI). Ik heb wat praktisch advies uitgepakt en genoteerd dat van toepassing zou moeten zijn op andere apps in het algemeen:

Data classes

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

waarschijnlijk heeft elke app een soort data class (ook bekend als DMOs, modellen, enz. afhankelijk van de context en waar ze in de architectuur van je app zitten). Het ding over gegevensobjecten is dat meestal op een bepaald punt zullen worden geladen of opgeslagen (geserialiseerd) in een ander medium, zoals netwerk (een HTTP-verzoek), een database (via een ORM), een JSON-bestand op schijf of in een Firebase-gegevensopslag.

veel van de Hulpmiddelen die het serialiseren en deserialiseren van deze velden vereenvoudigen, vertrouwen op reflectie. GSON, Retrofit — Firebase-ze inspecteren allemaal veldnamen in dataklassen en zetten ze om in een andere representatie (bijvoorbeeld: {"name”: "Sue”, "age”: 28}), hetzij voor transport of opslag. Hetzelfde gebeurt wanneer ze gegevens in een Java-object lezen — ze zien een sleutel-waarde paar "name”:”John” en proberen het toe te passen op een Java-object door een String name veld op te zoeken.

conclusie: we kunnen ProGuard geen velden op deze gegevensklassen laten hernoemen of verwijderen, omdat ze moeten overeenkomen met het seriële formaat. Het is een veilige gok om een @Keep annotatie toe te voegen op de hele klasse of een joker regel op al uw modellen:

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

waarschuwing: Het is mogelijk om een fout te maken bij het testen of uw app is gevoelig voor dit probleem. Als u bijvoorbeeld een object serialiseert op JSON en het op schijf opslaat in versie N van uw app zonder de juiste bewaarregels, kunnen de opgeslagen gegevens er als volgt uitzien: {"a”: "Sue”, "b”: 28}. Omdat ProGuard uw velden hernoemde naar a en b, zal alles lijken te werken, zullen gegevens correct worden opgeslagen en geladen.

echter, wanneer u uw app opnieuw bouwt en versie N+1 van uw app loslaat, kan ProGuard besluiten om uw velden te hernoemen naar iets anders, zoals c en d. Als gevolg daarvan worden eerder opgeslagen gegevens niet geladen.

u moet er in de eerste plaats voor zorgen dat u de juiste bewaarregels hebt.

Java-code aangeroepen vanaf native side (JNI)

De Standaard ProGuard-bestanden van Android (u moet ze altijd opnemen, ze hebben een aantal echt nuttige regels) bevatten al een regel voor methoden die zijn geïmplementeerd aan de native side (-keepclasseswithmembernames class * { native <methods>; }). Helaas is er geen catch-all manier om code aangeroepen te houden in de tegenovergestelde richting: van JNI naar Java.

met JNI is het heel goed mogelijk om een JVM object te construeren of een methode te vinden en aan te roepen op een JVM handvat van C/C++ code en in feite doet een van de bibliotheken die in Plaid worden gebruikt dat.

conclusie: omdat ProGuard alleen Java-klassen kan inspecteren, zal het geen gebruik weten dat in native code gebeurt. We moeten dit gebruik van klassen en leden expliciet behouden via een @Keep annotatie of -keep regel.

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

het openen van bronnen van JAR/APK

Android heeft zijn eigen middelen en activa systeem dat normaal gesproken geen probleem zou moeten zijn voor ProGuard. Echter, in gewone Java is er een ander mechanisme voor het laden van middelen rechtstreeks uit een JAR-bestand en sommige bibliotheken van derden zou kunnen worden met behulp van het, zelfs wanneer gecompileerd in Android-apps (in dat geval zullen ze proberen te laden van de APK).

het probleem is dat deze klassen meestal bronnen zoeken onder hun eigen pakketnaam (wat zich vertaalt naar een bestandspad in de JAR of APK). ProGuard kan pakketnamen hernoemen wanneer versluiering, dus na compilatie kan het gebeuren dat de klasse en haar resource-bestand niet langer in hetzelfde pakket in de uiteindelijke APK.

om laadbronnen op deze manier te identificeren, kunt u zoeken naar aanroepen naar Class.getResourceAsStream / getResource en ClassLoader.getResourceAsStream / getResource in uw code en in bibliotheken van derden waarvan u afhankelijk bent.

conclusie: we moeten de naam behouden van elke klasse die bronnen van de APK laadt met behulp van dit mechanisme.

in Plaid zijn er eigenlijk twee-één in de OkHttp-bibliotheek en één in Jsoup:

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

hoe te komen met regels voor bibliotheken van derden

in een ideale wereld zou elke afhankelijkheid die u gebruikt hun vereiste ProGuard-regels in de AAR leveren. Soms vergeten ze dit te doen of publiceren ze alleen potten, die geen gestandaardiseerde manier hebben om ProGuard regels te leveren.

in dat geval, voordat u begint met het debuggen van uw app en het bedenken van regels, vergeet niet om de documentatie te controleren. Sommige bibliotheek auteurs leveren aanbevolen ProGuard regels (zoals Retrofit gebruikt in Plaid) die u een hoop tijd en frustratie kan besparen. Helaas, veel bibliotheken niet (zoals het geval is met Jsoup en Bypass genoemd in dit artikel). Wees er ook van bewust dat in sommige gevallen de configuratie die bij de bibliotheek wordt geleverd alleen werkt met optimalisaties uitgeschakeld, dus als je ze aanzet, kun je je in onbekend terrein bevinden.

dus hoe kom je met regels als de bibliotheek ze niet levert?
Ik kan u slechts enkele aanwijzingen geven:

  1. lees de build output en logcat!
  2. Bouw waarschuwingen zal u vertellen welke -dontwarn regels toe te voegen
  3. ClassNotFoundExceptionMethodNotFoundException en FieldNotFoundException zal u vertellen welke -keep regels toe te voegen

Je moet blij zijn als je app crasht met ProGuard ingeschakeld — je moet ergens beginnen met je onderzoek 🙂

De slechtste klasse van problemen te debuggen zijn wanneer u de app werkt, maar bijvoorbeeld niet een scherm of niet laden van data van het netwerk.

dat is waar je een aantal van de scenario ‘ s moet overwegen die ik in dit artikel heb beschreven en je handen vuil moet maken, zelfs duiken in de code van derden en begrijpen waarom het zou kunnen mislukken, zoals wanneer het reflectie, introspectie of JNI gebruikt.

Debugging en stack traces

ProGuard zal standaard veel code attributen en verborgen metadata verwijderen die niet nodig zijn voor het uitvoeren van het programma . Sommige van deze zijn echt nuttig om de developer — bijvoorbeeld, wilt u misschien om te behouden bron bestand namen en regelnummers voor stacktraces te maken debuggen makkelijker:

-keepattributes SourceFile, LineNumberTable

Je moet ook niet vergeten om te redden van de ProGuard toewijzingen bestanden die worden geproduceerd als u een release versie en upload ze te Spelen om de-obfuscated stapel sporen van enige crashes ervaren door gebruikers.

Als u een debugger gaat toevoegen om methodecode te doorlopen in een Proguarde build van uw app, moet u ook de volgende attributen behouden om enkele debuginformatie over lokale variabelen te behouden (U hebt deze regel alleen nodig in uw debug build type):

-keepattributes LocalVariableTable, LocalVariableTypeTable

minified debug build type

de standaard build types zijn zo geconfigureerd dat debug geen Proguard uitvoert. Dat is logisch, want we willen itereren en snel compileren bij het ontwikkelen, maar willen nog steeds dat de release build om ProGuard te gebruiken zo klein en geoptimaliseerd mogelijk is.

maar om alle ProGuard problemen volledig te testen en te debuggen, is het goed om een aparte, minified debug build op te zetten als volgt:

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

met dit build type, zult u in staat zijn om de debugger te verbinden, UI-tests uit te voeren (ook op een CI server) of monkey test uw app voor mogelijke problemen op een build die zo dicht mogelijk bij uw release build ligt.

conclusie: Wanneer u ProGuard moet u altijd QA uw release bouwt grondig, hetzij door het hebben van end-to-end tests of handmatig gaan door alle schermen in uw app om te zien of er iets ontbreekt of crashen.

runtime annotaties, type introspection

ProGuard zal standaard alle annotaties en zelfs wat overtollige type informatie uit uw code verwijderen. Voor sommige bibliotheken is dat geen probleem — degenen die annotaties verwerken en code genereren tijdens het compileren (zoals Dagger 2 of Glide en nog veel meer) hebben deze annotaties later niet nodig als het programma draait.

er is een andere klasse tools die annotaties inspecteren of kijken naar type informatie van parameters en uitzonderingen tijdens runtime. Retrofit bijvoorbeeld doet dit door het onderscheppen van uw methode oproepen met behulp van een Proxy object, dan kijken naar annotaties en typ informatie om te beslissen wat te zetten of te lezen van de HTTP-aanvraag.

conclusie: soms is het nodig om type informatie en annotaties die worden gelezen tijdens runtime te bewaren, in tegenstelling tot compilatietijd. U kunt de attributen lijst bekijken in de ProGuard handleiding.

-keepattributes *Annotation*, Signature, Exception

Als u het standaard Android ProGuard configuratiebestand gebruikt (getDefaultProguardFile('proguard-android.txt')), worden de eerste twee opties — annotaties en handtekening — voor u opgegeven. Als u de standaard niet gebruikt, moet u ervoor zorgen dat u ze zelf toevoegt (het kan ook geen kwaad om ze gewoon te dupliceren als u weet dat ze een vereiste zijn voor uw app).

alles verplaatsen naar het standaardpakket

de optie -repackageclasses wordt standaard niet toegevoegd aan de Proguardconfiguratie. Als u al uw code verduistert en problemen met de juiste houdregels hebt opgelost, kunt u deze optie Toevoegen om Dex-grootte verder te verminderen. Het werkt door het verplaatsen van alle klassen naar de standaard (root) pakket, in wezen het vrijmaken van de ruimte die wordt ingenomen door strings zoals “com.bijvoorbeeld.myapp.somepackage”.

-repackageclasses

ProGuard-optimalisaties

zoals ik al eerder zei, kan ProGuard 3 dingen voor u doen:

  1. Het verwijdert ongebruikte code,
  2. hernoemt identifiers om de code kleiner te maken,
  3. voert hele programma-optimalisaties uit.

zoals ik het zie, zou iedereen moeten proberen zijn build te configureren om 1 te krijgen. en 2. werken.

om te ontgrendelen 3. (extra optimalisaties), moet je een ander standaard ProGuard configuratiebestand gebruiken. Verander de proguard-android.txt parameter naar proguard-android-optimize.txt in uw build.gradle:

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

Dit zal uw release langzamer maken, maar zal mogelijk uw app sneller laten draaien en de code nog verder verkleinen, dankzij optimalisaties zoals method inlining, klasse samenvoegen en meer agressieve code verwijderen. Wees er echter op voorbereid dat het nieuwe en moeilijk te diagnosticeren bugs zou kunnen introduceren, dus gebruik het met de nodige voorzichtigheid en als er iets niet werkt, moet u bepaalde optimalisaties uitschakelen of het gebruik van de optimalisatieconfig helemaal uitschakelen.

in het geval van Plaid verstoorden ProGuard-optimalisaties hoe Retrofit Proxy-objecten gebruikt zonder concrete implementaties, en verwijderden enkele methodeparameters die daadwerkelijk nodig waren. Ik moest deze regel aan mijn configuratie toevoegen:

-optimizations !method/removal/parameter

u kunt een lijst vinden met mogelijke optimalisaties en hoe u ze kunt uitschakelen in de ProGuard-handleiding.

wanneer @Keep en-keep

@Keep ondersteuning wordt daadwerkelijk geïmplementeerd als een stel -keep regels in het standaard Android ProGuard regels bestand, dus ze zijn in wezen gelijkwaardig. Het specificeren van -keep regels is flexibeler omdat het jokertekens biedt, u kunt ook verschillende varianten gebruiken die iets andere dingen doen (-keepnames-keepclasseswithmembers en meer).

wanneer een eenvoudige” keep this class “of” keep this method”regel nodig is, geef ik de voorkeur aan de eenvoud van het toevoegen van een@Keep annotatie op de klasse of het lid, omdat het dicht bij de code blijft, bijna als documentatie.

als een andere Ontwikkelaar na mij de code wil refactor, zullen ze onmiddellijk weten dat een klasse/lid gemarkeerd met @Keep speciale behandeling vereist, zonder eraan te hoeven denken om de ProGuard configuratie te raadplegen en het risico te lopen iets te breken. Ook de meeste code refactorings in de IDE moeten de @Keep annotatie met de klasse automatisch behouden.

Plaid stats

Hier zijn enkele statistieken van Plaid, die laten zien hoeveel code Ik heb kunnen verwijderen met behulp van ProGuard. Op een complexere app met meer afhankelijkheden en een grotere DEX kan de besparing nog substantiëler zijn.