Articles

Pratiche ProGuard regole esempi

Wojtek Kaliciński
Wojtek Kaliciński

Seguire

Feb 20, 2018 · 9 min leggere

In un mio precedente articolo ho spiegato il motivo per cui tutti dovrebbero utilizzare ProGuard per le loro applicazioni Android, come attivare e di che tipo di errori che si possono incontrare quando si fa così. C’era molta teoria coinvolta, poiché penso che sia importante capire i principi sottostanti per essere pronti ad affrontare qualsiasi potenziale problema.

Ho anche parlato in un articolo separato del problema molto specifico della configurazione di ProGuard per una build di app istantanea.

In questa parte, vorrei parlare degli esempi pratici delle regole ProGuard su un’app di medie dimensioni: Plaid di Nick Butcher.

Plaid in realtà si è rivelato un ottimo argomento per la ricerca dei problemi ProGuard, in quanto contiene un mix di librerie di terze parti che utilizzano cose come l’elaborazione delle annotazioni e la generazione di codice, la riflessione, il caricamento delle risorse java e il codice nativo (JNI). Ho estratto e annotato alcuni consigli pratici che dovrebbero applicarsi ad altre app in generale:

Classi di dati

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

Probabilmente ogni app ha una sorta di classe di dati (nota anche come DMOs, modelli, ecc. a seconda del contesto e di dove si trovano nell’architettura della tua app). La cosa sugli oggetti dati è che di solito ad un certo punto verranno caricati o salvati (serializzati) in qualche altro mezzo, come la rete (una richiesta HTTP), un database (attraverso un ORM), un file JSON su disco o in un archivio dati Firebase.

Molti degli strumenti che semplificano la serializzazione e la deserializzazione di questi campi si basano sulla riflessione. GSON, Retrofit, Firebase: tutti ispezionano i nomi dei campi nelle classi di dati e li trasformano in un’altra rappresentazione (ad esempio: {"name”: "Sue”, "age”: 28}), sia per il trasporto che per l’archiviazione. La stessa cosa accade quando leggono i dati in un oggetto Java-vedono una coppia chiave-valore"name”:”John” e cercano di applicarlo a un oggetto Java cercando un campoString name .

Conclusione: Non possiamo lasciare che ProGuard rinomini o rimuova alcun campo su queste classi di dati, poiché devono corrispondere al formato serializzato. È una scommessa sicura aggiungere un’annotazione@Keep sull’intera classe o una regola jolly su tutti i modelli:

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

Avviso: È possibile commettere un errore durante il test se la tua app è suscettibile a questo problema. Ad esempio, se si serializza un oggetto su JSON e lo si salva su disco nella versione N dell’app senza le regole keep appropriate, i dati salvati potrebbero essere simili a questi: {"a”: "Sue”, "b”: 28}. Poiché ProGuard ha rinominato i tuoi campi ina eb, tutto sembrerà funzionare, i dati verranno salvati e caricati correttamente.

Tuttavia, quando costruisci di nuovo la tua app e rilasci la versione N+1 della tua app, ProGuard potrebbe decidere di rinominare i tuoi campi in qualcosa di diverso, comec ed. Di conseguenza, i dati salvati in precedenza non verranno caricati.

È necessario assicurarsi di avere le regole di mantenimento corrette in primo luogo.

Codice Java chiamato dal lato nativo (JNI)

I file ProGuard predefiniti di Android (dovresti sempre includerli, hanno alcune regole davvero utili) contengono già una regola per i metodi implementati sul lato nativo (-keepclasseswithmembernames class * { native <methods>; }). Sfortunatamente non esiste un modo per mantenere il codice invocato nella direzione opposta: da JNI a Java.

Con JNI è del tutto possibile costruire un oggetto JVM o trovare e chiamare un metodo su un handle JVM dal codice C/C++ e in effetti, una delle librerie utilizzate in Plaid lo fa.

Conclusione: Poiché ProGuard può ispezionare solo le classi Java, non saprà di eventuali usi che si verificano nel codice nativo. Dobbiamo mantenere esplicitamente tali usi di classi e membri tramite un’annotazione@Keep o-keep regola.

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

Aprire risorse da JAR/APK

Android ha un proprio sistema di risorse e risorse che normalmente non dovrebbe essere un problema per ProGuard. Tuttavia, in Java semplice c’è un altro meccanismo per caricare le risorse direttamente da un file JAR e alcune librerie di terze parti potrebbero usarlo anche quando compilato in app Android (in tal caso proveranno a caricare dall’APK).

Il problema è che di solito queste classi cercheranno risorse con il proprio nome di pacchetto (che si traduce in un percorso di file nel JAR o nell’APK). ProGuard può rinominare i nomi dei pacchetti quando si offuscano, quindi dopo la compilazione potrebbe accadere che la classe e il suo file di risorse non siano più nello stesso pacchetto nell’APK finale.

Per identificare il caricamento delle risorse in questo modo, è possibile cercare le chiamate a Class.getResourceAsStream / getResourcee ClassLoader.getResourceAsStream / getResource nel codice e in qualsiasi libreria di terze parti da cui si dipende.

Conclusione: Dovremmo mantenere il nome di qualsiasi classe che carica risorse dall’APK usando questo meccanismo.

In Plaid, ce ne sono in realtà due — uno nella libreria OkHttp e uno in Jsoup:

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

Come creare regole per librerie di terze parti

In un mondo ideale, ogni dipendenza che usi fornirebbe le regole ProGuard richieste nell’AAR. A volte dimenticano di farlo o pubblicano solo JAR, che non hanno un modo standardizzato per fornire le regole ProGuard.

In tal caso, prima di iniziare a eseguire il debug della tua app e a creare regole, ricorda di controllare la documentazione. Alcuni autori di librerie forniscono regole ProGuard consigliate (come il Retrofit utilizzato in Plaid) che possono farti risparmiare un sacco di tempo e frustrazione. Sfortunatamente, molte librerie non lo fanno (come nel caso di Jsoup e Bypass menzionati in questo articolo). Tieni anche presente che in alcuni casi la configurazione fornita con la libreria funzionerà solo con le ottimizzazioni disabilitate, quindi se le stai attivando potresti trovarti in un territorio inesplorato.

Quindi, come trovare le regole quando la libreria non le fornisce?
Posso solo darti alcuni suggerimenti:

  1. Leggi l’output di build e logcat!
  2. Costruire avvisi vi dirà che il -dontwarn regole di aggiungere
  3. ClassNotFoundExceptionMethodNotFoundException e FieldNotFoundException vi dirà che il -keep regole di aggiungere

Si dovrebbe essere contento quando l’app si blocca con ProGuard attivato, avrai da qualche parte per iniziare la sua indagine 🙂

La peggiore classe di problemi di debug sono quando l’app funziona, ma ad esempio non mostra una schermata o non carica i dati dalla rete.

È qui che devi considerare alcuni degli scenari che ho descritto in questo articolo e sporcarti le mani, anche immergendoti nel codice di terze parti e capendo perché potrebbe fallire, ad esempio quando usa reflection, introspection o JNI.

Tracce di debug e stack

ProGuard rimuoverà per impostazione predefinita molti attributi di codice e metadati nascosti che non sono necessari per l’esecuzione del programma . Alcuni di questi sono effettivamente utile per lo sviluppatore, ad esempio, si potrebbe desiderare di mantenere il file di origine nomi e i numeri di riga per le tracce dello stack per facilitare il debugging:

-keepattributes SourceFile, LineNumberTable

Si dovrebbe anche ricordarsi di salvare il ProGuard mapping del file prodotto quando si genera una versione e caricare loro di Giocare per ottenere smascherati tracce dello stack da crash sperimentato dagli utenti.

Se si sta andando a collegare un debugger per il passaggio attraverso il codice del metodo in un ProGuarded build dell’applicazione, si dovrebbe anche mantenere i seguenti attributi di conservare alcune informazioni di debug su variabili locali (solo bisogno di questa linea in un debug tipo di build):

-keepattributes LocalVariableTable, LocalVariableTypeTable

Minified build di debug tipo

Il default costruire tipi sono configurati in modo tale che il debug non funziona ProGuard. Ciò ha senso, perché vogliamo iterare e compilare velocemente durante lo sviluppo, ma vogliamo comunque che la build di rilascio usi ProGuard per essere il più piccola e ottimizzata possibile.

Ma per testare ed eseguire il debug completo di qualsiasi problema ProGuard, è bene impostare una build di debug separata e minificata come questa:

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

Con questo tipo di build, sarai in grado di connettere il debugger, eseguire test dell’interfaccia utente (anche su un server CI) o testare la tua app per possibili problemi su una build il più vicino possibile alla tua build di rilascio.

Conclusione: Quando si utilizza ProGuard, è necessario eseguire sempre il QA del rilascio a fondo, eseguendo test end-to-end o passando manualmente attraverso tutte le schermate della propria app per vedere se manca qualcosa o si blocca.

Annotazioni di runtime, tipo introspection

ProGuard per impostazione predefinita rimuoverà tutte le annotazioni e anche alcune informazioni di tipo in eccesso dal codice. Per alcune librerie questo non è un problema: quelle che elaborano annotazioni e generano codice in fase di compilazione (come Dagger 2 o Glide e molte altre) potrebbero non aver bisogno di queste annotazioni in seguito quando viene eseguito il programma.

Esiste un’altra classe di strumenti che effettivamente ispezionano le annotazioni o esaminano le informazioni sul tipo di parametri ed eccezioni in fase di runtime. Retrofit ad esempio lo fa intercettando le chiamate al metodo utilizzando un oggetto Proxy, quindi guardando le annotazioni e digitando le informazioni per decidere cosa mettere o leggere dalla richiesta HTTP.

Conclusione: a volte è necessario conservare le informazioni sul tipo e le annotazioni che vengono lette in fase di esecuzione, anziché in fase di compilazione. È possibile controllare l’elenco degli attributi nel manuale ProGuard.

-keepattributes *Annotation*, Signature, Exception

Se stai utilizzando il file di configurazione ProGuard Android predefinito (getDefaultProguardFile('proguard-android.txt')), le prime due opzioni — Annotazioni e Firma — sono specificate per te. Se non stai usando l’impostazione predefinita, devi assicurarti di aggiungerli tu stesso (inoltre non fa male duplicarli solo se sai che sono un requisito per la tua app).

Spostare tutto nel pacchetto predefinito

L’opzione -repackageclasses non viene aggiunta di default nella configurazione di ProGuard. Se stai già offuscando il tuo codice e hai risolto eventuali problemi con le regole keep corrette, puoi aggiungere questa opzione per ridurre ulteriormente le dimensioni di DEX. Funziona spostando tutte le classi sul pacchetto predefinito (root), essenzialmente liberando lo spazio occupato da stringhe come “com.esempio.myapp.alcuni pacchetti”.

-repackageclasses

Proguard optimizations

Come ho detto prima, ProGuard può fare 3 cose per te:

  1. elimina il codice inutilizzato,
  2. rinomina gli identificatori per rendere il codice più piccolo,
  3. esegue le ottimizzazioni dell’intero programma.

Per come la vedo io, tutti dovrebbero provare a configurare la loro build per ottenere 1. e 2. lavoro.

Per sbloccare 3. (ottimizzazioni aggiuntive), è necessario utilizzare un file di configurazione ProGuard predefinito diverso. Cambia il parametro proguard-android.txt in proguard-android-optimize.txt nel tuo build.gradle:

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

Questo renderà la tua versione più lenta, ma potenzialmente renderà la tua app più veloce e ridurrà ulteriormente le dimensioni del codice, grazie per ottimizzazioni come l’inlining del metodo, l’unione di classi e la rimozione del codice più aggressiva. Essere preparati tuttavia, che potrebbe introdurre nuovi e difficili da diagnosticare bug, quindi usalo con cautela e se qualcosa non funziona, assicurati di disabilitare alcune ottimizzazioni o disabilitare del tutto l’uso della configurazione di ottimizzazione.

Nel caso di Plaid, le ottimizzazioni ProGuard hanno interferito con il modo in cui Retrofit utilizza oggetti Proxy senza implementazioni concrete e hanno rimosso alcuni parametri del metodo che erano effettivamente richiesti. Ho dovuto aggiungere questa linea alla mia configurazione:

-optimizations !method/removal/parameter

È possibile trovare un elenco di possibili ottimizzazioni e come disabilitarle nel manuale di ProGuard.

Quando usare @Keep and-keep

@Keep il supporto è effettivamente implementato come un gruppo di regole-keep nel file di regole ProGuard Android predefinito, quindi sono essenzialmente equivalenti. Specificando le regole-keep è più flessibile in quanto offre caratteri jolly, puoi anche usare diverse varianti che fanno cose leggermente diverse (-keepnames-keepclasseswithmembers e altro).

Ogni volta che è necessaria una semplice regola “mantieni questa classe” o “mantieni questo metodo”, in realtà preferisco la semplicità di aggiungere un’annotazione@Keep sulla classe o sul membro, poiché rimane vicino al codice, quasi come la documentazione.

Se qualche altro sviluppatore che viene dopo di me vuole refactoring il codice, sapranno immediatamente che una classe/membro contrassegnata con @Keep richiede una gestione speciale, senza dover ricordare di consultare la configurazione di ProGuard e rischiare di rompere qualcosa. Inoltre, la maggior parte dei refactoring del codice nell’IDE dovrebbe mantenere automaticamente l’annotazione @Keep con la classe.

Statistiche Plaid

Ecco alcune statistiche di Plaid, che mostrano quanto codice sono riuscito a rimuovere usando ProGuard. Su un’app più complessa con più dipendenze e un DEX più grande i risparmi possono essere ancora più consistenti.