Articles

praktiska exempel på Proguardregler

Wojtek Kalici Jacobski
Wojtek Kalici Jacobski

följ
feb 20, 2018 · 9 min läs

i min tidigare artikel förklarade jag varför alla ska använda ProGuard för sina Android-appar, hur man aktiverar det och vilken typ av fel du kan stöta på när du gör det. Det var mycket teori inblandad, eftersom jag tycker att det är viktigt att förstå de underliggande principerna för att vara beredd att hantera eventuella problem.

jag pratade också i en separat artikel om det mycket specifika problemet med att konfigurera ProGuard för en omedelbar Appbyggnad.

i den här delen skulle jag vilja prata om de praktiska exemplen på ProGuard-regler på en medelstor exempelapp: Plaid av Nick Butcher.

Plaid visade sig faktiskt vara ett bra ämne för att undersöka ProGuard-problem, eftersom det innehåller en blandning av 3: e parts bibliotek som använder saker som anteckningsbehandling och kodgenerering, reflektion, java-resursbelastning och inbyggd kod (JNI). Jag extraherade och noterade några praktiska råd som borde gälla för andra appar i allmänhet:

dataklasser

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

förmodligen har varje app någon form av dataklass (även känd som DMO, modeller etc. beroende på sammanhang och var de sitter i appens arkitektur). Saken med dataobjekt är att de vanligtvis någon gång kommer att laddas eller sparas (serialiseras) i något annat medium, till exempel nätverk (en HTTP-begäran), en databas (via en ORM), en JSON-fil på disk eller i en Firebase-datalager.

många av verktygen som förenklar serialisering och deserialisering av dessa fält är beroende av reflektion. GSON, Retrofit, Firebase-de inspekterar alla fältnamn i dataklasser och gör dem till en annan representation (till exempel: {"name”: "Sue”, "age”: 28}), antingen för transport eller lagring. Samma sak händer när de läser data i ett Java — objekt-de ser ett nyckelvärdespar "name”:”John” och försöker tillämpa det på ett Java-objekt genom att leta upp ett String name fält.

slutsats: Vi kan inte låta ProGuard byta namn på eller ta bort några fält i dessa dataklasser, eftersom de måste matcha det serialiserade formatet. Det är en säker satsning att lägga till en@Keep anteckning på hela klassen eller en jokerteckenregel på alla dina modeller:

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

Varning: Det är möjligt att göra ett misstag när du testar om din app är mottaglig för det här problemet. Om du till exempel serialiserar ett objekt till JSON och sparar det på disk i version N av din app utan rätt behållningsregler kan de sparade data se ut så här: {"a”: "Sue”, "b”: 28}. Eftersom ProGuard döpte om dina fält till a och b, verkar allt fungera, data sparas och laddas korrekt.

men när du bygger din app igen och släpper version N+1 av din app kan ProGuard besluta att byta namn på dina fält till något annat, till exempelc ochd. Som ett resultat kommer data som sparats tidigare inte att laddas.

Du måste se till att du har rätt behållningsregler i första hand.

Java-kod som heter från native side (JNI)

Android standard ProGuard-filer (du bör alltid inkludera dem, de har några riktigt användbara regler) innehåller redan en regel för metoder som implementeras på den ursprungliga sidan (-keepclasseswithmembernames class * { native <methods>; }). Tyvärr finns det inget sätt att hålla koden åberopad i motsatt riktning: från JNI till Java.

med JNI är det helt möjligt att konstruera ett JVM-objekt eller hitta och ringa en metod på ett JVM-handtag från C/C++ – kod och i själva verket gör ett av biblioteken som används i Plaid det.

slutsats: eftersom ProGuard bara kan inspektera Java-klasser, kommer det inte att veta om några användningar som händer i inbyggd kod. Vi måste uttryckligen behålla sådana användningar av klasser och medlemmar via en @Keep annotation eller -keep regel.

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

öppna resurser från JAR/APK

Android har sina egna resurser och tillgångssystem som normalt inte borde vara ett problem för ProGuard. Men i vanlig Java finns det en annan mekanism för att ladda resurser direkt från en JAR-fil och vissa tredjepartsbibliotek kan använda den även när de sammanställs i Android-appar (i så fall kommer de att försöka ladda från APK).

problemet är att dessa klasser vanligtvis letar efter resurser under sitt eget Paketnamn (som översätts till en filväg i burken eller APK). ProGuard kan byta namn på Paketnamn när obfuscating, så efter kompilering kan det hända att klassen och dess resursfilen inte längre finns i samma paket i den slutliga APK.

för att identifiera inläsningsresurser på detta sätt kan du leta efter samtal till Class.getResourceAsStream / getResource och ClassLoader.getResourceAsStream / getResource I din kod och i alla tredjepartsbibliotek du är beroende av.

slutsats: vi bör behålla namnet på någon klass som laddar resurser från APK med denna mekanism.

i Plaid finns det faktiskt två-en i OkHttp-biblioteket och en i Jsoup:

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

hur man kommer fram till regler för tredjepartsbibliotek

i en idealisk värld skulle varje beroende du använder leverera sina nödvändiga ProGuard-regler i AAR. Ibland glömmer de att göra detta eller bara publicera burkar, som inte har ett standardiserat sätt att leverera ProGuard-regler.

i så fall, innan du börjar felsöka din app och komma med regler, kom ihåg att kontrollera dokumentationen. Vissa biblioteksförfattare tillhandahåller rekommenderade ProGuard-regler (som eftermontering som används i Plaid) vilket kan spara mycket tid och frustration. Tyvärr gör många bibliotek inte (som det är fallet med Jsoup och Bypass som nämns i den här artikeln). Tänk också på att konfigurationen som medföljer biblioteket i vissa fall bara fungerar med optimeringar inaktiverade, så om du aktiverar dem kan du vara i okänt territorium.

Så hur kommer man fram till regler när biblioteket inte levererar dem?
Jag kan bara ge dig några tips:

  1. Läs byggutgången och logcat!
  2. Byggvarningar kommer att berätta vilka -dontwarn regler för att lägga till
  3. ClassNotFoundExceptionMethodNotFoundException och FieldNotFoundException kommer att berätta vilka -keep regler för att lägga till

Du borde vara glad när din app kraschar med ProGuard aktiverat — du har någonstans att starta din undersökning 🙂

den värsta klassen av problem att felsöka är när du app fungerar, men visar till exempel inte en skärm eller laddar inte data från nätverket.

det är där du måste överväga några av de scenarier som jag beskrev i den här artikeln och få dina händer smutsiga, till och med dyka in i tredjepartskoden och förstå varför det kan misslyckas, till exempel när det använder reflektion, introspektion eller JNI.

felsökning och stackspår

ProGuard tar som standard bort många kodattribut och dolda metadata som inte krävs för programkörning . Några av dem är faktiskt användbara för utvecklaren — till exempel kanske du vill behålla källfilnamn och radnummer för stackspår för att göra felsökning enklare:

-keepattributes SourceFile, LineNumberTable

Du bör också komma ihåg att spara ProGuard mappings-filerna som produceras när du bygger en release-version och laddar upp dem för att spela för att få de-obfuscated stack-spår från eventuella kraschar som användare.

Om du ska bifoga en debugger för att gå igenom metodkod i en ProGuarded build av din app, bör du också behålla följande attribut för att behålla viss felsökningsinformation om lokala variabler (du behöver bara den här raden i din debug byggtyp):

-keepattributes LocalVariableTable, LocalVariableTypeTable

minified debug build type

standardbyggnadstyperna är konfigurerade så att debug inte kör ProGuard. Det är vettigt, eftersom vi vill iterera och kompilera snabbt när vi utvecklar, men ändå vill att release build ska använda ProGuard för att vara så liten och optimerad som möjligt.

men för att fullt ut testa och felsöka eventuella ProGuard-problem är det bra att skapa en separat, minifierad felsökningsbyggnad så här:

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

med den här byggtypen kan du ansluta felsökaren, köra UI-tester (även på en CI-server) eller apa testa din app för eventuella problem på en byggnad som är så nära din release build som möjligt.

slutsats: När du använder ProGuard bör du alltid QA din release bygger noggrant, antingen genom att ha end-to-end tester eller manuellt gå igenom alla skärmar i din app för att se om något saknas eller kraschar.

Runtime-anteckningar, typ introspektion

ProGuard tar som standard bort alla anteckningar och till och med viss information om överskottstyp från din kod. För vissa bibliotek är det inte ett problem — de som behandlar anteckningar och genererar kod vid kompileringstiden (som Dagger 2 eller Glide och många fler) kanske inte behöver dessa anteckningar senare när programmet körs.

det finns en annan klass av verktyg som faktiskt inspekterar anteckningar eller tittar på typinformation om parametrar och undantag vid körning. Eftermontering till exempel gör detta genom att avlyssna dina metodsamtal med hjälp av ett Proxy objekt, sedan titta på anteckningar och skriv information för att bestämma vad du ska lägga eller läsa från HTTP-begäran.

slutsats: ibland är det nödvändigt att behålla typinformation och anteckningar som läses vid körning, i motsats till kompileringstid. Du kan kolla in attributlistan i ProGuard-manualen.

-keepattributes *Annotation*, Signature, Exception

Om du använder standard Android ProGuard konfigurationsfilen (getDefaultProguardFile('proguard-android.txt')), de två första alternativen — anteckningar och signatur — anges för dig. Om du inte använder standard måste du se till att lägga till dem själv (det gör inte heller ont för att bara duplicera dem om du vet att de är ett krav för din app).

flytta allt till standardpaketet

alternativet-repackageclasses läggs inte till som standard i ProGuard-konfigurationen. Om du redan fördunklar din kod och har åtgärdat några problem med korrekta keep-regler kan du lägga till det här alternativet för att ytterligare minska DEX-storleken. Det fungerar genom att flytta alla klasser till standardpaketet (root), vilket i huvudsak frigör utrymmet som tas upp av strängar som ”com.exempel.myapp.somepackage”.

-repackageclasses

ProGuard optimeringar

som jag nämnde tidigare kan ProGuard göra 3 saker för dig:

  1. Det blir av med oanvänd kod,
  2. byter namn på identifierare för att göra koden mindre,
  3. utför hela programoptimeringar.

så som jag ser det, bör alla försöka konfigurera sin byggnad för att få 1. och 2. arbeta.

för att låsa upp 3. (ytterligare optimeringar), måste du använda en annan standard ProGuard konfigurationsfil. Ändra proguard-android.txt parameter till proguard-android-optimize.txt I din build.gradle:

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

detta kommer att göra din release bygga långsammare, men kommer potentiellt att göra din app köra snabbare och minska kodstorleken ytterligare, tack till optimeringar som metod inlining, klass sammanslagning och mer aggressiv kod borttagning. Var dock beredd att det kan introducera nya och svåra att diagnostisera buggar, så använd det med försiktighet och om något inte fungerar, var noga med att inaktivera vissa optimeringar eller inaktivera användningen av optimeringskonfigurationen helt och hållet.

När det gäller Plaid störde ProGuard-optimeringar hur Retrofit använder Proxyobjekt utan konkreta implementeringar och avlägsnade några metodparametrar som faktiskt krävdes. Jag var tvungen att lägga till den här raden i min konfiguration:

-optimizations !method/removal/parameter

Du kan hitta en lista över möjliga optimeringar och hur du inaktiverar dem i ProGuard-manualen.

när du ska använda @Keep and-keep

@Keep stöd implementeras faktiskt som en massa-keep regler i standardfilen för Android ProGuard rules, så de är i huvudsak likvärdiga. Att ange-keep regler är mer flexibla eftersom det erbjuder jokertecken, du kan också använda olika varianter som gör lite olika saker (-keepnames-keepclasseswithmembers och mer).

När en enkel” keep this class ”eller” keep this method” – regel behövs, föredrar jag faktiskt enkelheten att lägga till en@Keep anteckning på klassen eller medlemmen, eftersom den stannar nära koden, nästan som dokumentation.

Om någon annan utvecklare som kommer efter mig vill refactor koden, kommer de att veta omedelbart att en klass / medlem märkt med @Keep kräver speciell hantering, utan att behöva komma ihåg att konsultera ProGuard-konfigurationen och riskera att bryta något. Även de flesta kodrefaktorer i IDE bör behålla @Keep annotering med klassen automatiskt.

Plaid stats

här är några statistik från Plaid, som visar hur mycket kod jag lyckades ta bort med ProGuard. På en mer komplex app med fler beroenden och en större DEX kan besparingarna bli ännu större.