Articles

exemple practice de reguli ProGuard

Wojtek Kalici Xvski
Wojtek Kalici Xvski

urmați
februarie 20, 2018 · 9 min citit

în articolul meu anterior am explicat de ce toată lumea ar trebui să folosească ProGuard pentru aplicațiile lor Android, cum să-l activați și ce fel de erori s-ar putea întâlni atunci când face acest lucru. A fost implicată o mulțime de teorii, deoarece cred că este important să înțelegem principiile care stau la baza pentru a fi pregătiți să facem față oricăror probleme potențiale.

de asemenea, am vorbit într-un articol separat despre problema foarte specifică a configurării ProGuard pentru o construire instantanee a aplicațiilor.

în această parte, aș dori să vorbesc despre exemplele practice ale regulilor ProGuard pe o aplicație de probă de dimensiuni medii: Plaid de Nick Butcher.

Plaid s-a dovedit a fi un subiect excelent pentru cercetarea problemelor ProGuard, deoarece conține un amestec de biblioteci 3rd party care folosesc lucruri precum procesarea adnotărilor și generarea de coduri, reflecția, încărcarea resurselor java și codul nativ (JNI). Am extras și notat câteva sfaturi practice care ar trebui să se aplice altor aplicații în general:

clase de date

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

probabil fiecare aplicație are un fel de clasă de date (cunoscută și sub numele de DMOs, modele etc. în funcție de context și de locul în care se află în arhitectura aplicației dvs.). Lucrul despre obiectele de date este că, de obicei, la un moment dat vor fi încărcate sau salvate (serializate) într-un alt mediu, cum ar fi rețea (o cerere HTTP), o bază de date (printr-un ORM), un fișier JSON pe disc sau într-un magazin de date Firebase.multe dintre instrumentele care simplifică serializarea și deserializarea acestor câmpuri se bazează pe reflecție. GSON, Retrofit, Firebase-toate inspectează numele câmpurilor din clasele de date și le transformă într-o altă reprezentare (de exemplu: {"name”: "Sue”, "age”: 28}), fie pentru transport, fie pentru depozitare. Același lucru se întâmplă atunci când citesc date într — un obiect Java-văd o pereche cheie-valoare "name”:”John” și încearcă să o aplice unui obiect Java căutând un câmp String name .

concluzie: Nu putem lăsa ProGuard să redenumească sau să elimine niciun câmp din aceste clase de date, deoarece acestea trebuie să se potrivească cu formatul serializat. Este un pariu sigur să adăugați o@Keep adnotare pe întreaga clasă sau o regulă wildcard pe toate modelele dvs.:

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

avertisment: Este posibil să faceți o greșeală atunci când testați dacă aplicația dvs. este susceptibilă la această problemă. De exemplu, dacă serializați un obiect pe JSON și îl salvați pe disc în versiunea N a aplicației dvs. fără regulile de păstrare corespunzătoare, datele salvate ar putea arăta astfel: {"a”: "Sue”, "b”: 28}. Deoarece ProGuard a redenumit câmpurile dvs. în a și b, totul va părea să funcționeze, datele vor fi salvate și încărcate corect.

cu toate acestea, atunci când construiți din nou aplicația și lansați versiunea N+1 a aplicației, ProGuard ar putea decide să redenumiți câmpurile în ceva diferit, cum ar fi c și d. Ca urmare, datele salvate anterior nu se vor încărca.

trebuie să vă asigurați că aveți regulile de păstrare corespunzătoare în primul rând.

cod Java apelat din partea nativă (JNI)

fișierele Proguard implicite Android (ar trebui să le includeți întotdeauna, au câteva reguli cu adevărat utile) conțin deja o regulă pentru metodele care sunt implementate pe partea nativă (-keepclasseswithmembernames class * { native <methods>; }). Din păcate, nu există nicio modalitate de a păstra codul invocat în direcția opusă: de la JNI la Java.

cu JNI este în întregime posibil să construiți un obiect JVM sau să găsiți și să apelați o metodă pe un mâner JVM din Codul C / C++ și, de fapt, una dintre bibliotecile utilizate în Plaid face asta.

concluzie: deoarece ProGuard poate inspecta numai clase Java, nu va ști despre orice utilizări care se întâmplă în codul nativ. Trebuie să păstrăm în mod explicit astfel de utilizări ale claselor și membrilor printr-o @Keep adnotare sau -keep regulă.

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

deschiderea resurselor din JAR/APK

Android are propriile resurse și active sistem care în mod normal nu ar trebui să fie o problemă pentru ProGuard. Cu toate acestea, în Java simplu există un alt mecanism pentru încărcarea resurselor direct dintr-un fișier JAR și unele biblioteci terțe ar putea să-l folosească chiar și atunci când sunt compilate în aplicații Android (în acest caz vor încerca să se încarce din APK).

problema este că, de obicei, aceste clase vor căuta resurse sub propriul nume de pachet (care se traduce într-o cale de fișier în borcan sau APK). ProGuard poate redenumi numele pachetelor atunci când se ascunde, astfel încât după compilare s-ar putea întâmpla ca clasa și fișierul său de resurse să nu mai fie în același pachet în APK-ul final.

pentru a identifica resursele de încărcare în acest fel, puteți căuta apeluri către Class.getResourceAsStream / getResource și ClassLoader.getResourceAsStream / getResource în codul dvs. și în orice biblioteci terțe de care depindeți.

concluzie: ar trebui să păstrăm numele oricărei clase care încarcă resurse din APK folosind acest mecanism.

în carouri, există de fapt două — una în biblioteca OkHttp și una în Jsoup:

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

cum să veniți cu reguli pentru bibliotecile terțe

într-o lume ideală, fiecare dependență pe care o utilizați ar furniza Regulile ProGuard necesare în AAR. Uneori uită să facă acest lucru sau publică doar borcane, care nu au un mod standardizat de a furniza reguli ProGuard.

în acest caz, înainte de a începe să depanați aplicația și să veniți cu reguli, nu uitați să verificați documentația. Unii autori de bibliotecă furnizează reguli ProGuard recomandate (cum ar fi Retrofit utilizate în carouri), care vă pot economisi mult timp și frustrare. Din păcate, multe biblioteci nu (cum ar fi cazul Jsoup și Bypass menționate în acest articol). De asemenea, rețineți că, în unele cazuri, configurația furnizată împreună cu Biblioteca va funcționa numai cu optimizări dezactivate, deci dacă le activați, este posibil să vă aflați pe un teritoriu neexplorat.

deci, cum să vină cu reguli atunci când biblioteca nu le furnizează?
eu pot să vă dau doar câteva indicii:

  1. Citește ieșire construi și logcat!
  2. construi avertismente vă va spune care-dontwarn reguli pentru a adăuga
  3. ClassNotFoundExceptionMethodNotFoundException șiFieldNotFoundException vă va spune care-keep reguli de adăugat

ar trebui să vă bucurați când aplicația dvs. se blochează cu ProGuard activat — veți avea undeva să începeți investigația:)

cea mai gravă clasă de probleme de depanare sunt atunci când aplicația funcționează, dar, de exemplu, nu afișează un ecran sau nu încarcă date din rețea.

aici trebuie să luați în considerare unele dintre scenariile pe care le-am descris în acest articol și să vă murdăriți mâinile, chiar scufundându-vă în codul terților și înțelegând de ce ar putea eșua, cum ar fi atunci când folosește reflecție, introspecție sau JNI.

depanare și stivă urme

ProGuard va elimina în mod implicit multe atribute de cod și metadate ascunse care nu sunt necesare pentru executarea programului . Unele dintre acestea sunt de fapt utile dezvoltatorului — de exemplu, poate doriți să păstrați numele fișierelor sursă și numerele de linie pentru urmele stivei pentru a facilita depanarea:

-keepattributes SourceFile, LineNumberTable

de asemenea, ar trebui să vă amintiți să salvați fișierele de mapări ProGuard produse atunci când construiți o versiune de lansare și să le încărcați pentru a reda pentru a obține urme de stivă dezumflate din orice blocări experimentate de utilizatori.

Dacă aveți de gând să atașați un depanator pentru a trece prin codul metodei într-o versiune ProGuarded a aplicației dvs., ar trebui să păstrați și următoarele atribute pentru a păstra unele informații de depanare despre variabilele locale (aveți nevoie doar de această linie în debug tip de construcție):

-keepattributes LocalVariableTable, LocalVariableTypeTable

minified debug Build Type

tipurile implicite de compilare sunt configurate astfel încât depanarea să nu ruleze ProGuard. Acest lucru are sens, pentru că vrem să repetăm și să compilăm rapid atunci când dezvoltăm, dar totuși dorim ca versiunea de lansare să utilizeze ProGuard să fie cât mai mică și optimizată posibil.

dar pentru a testa și depana complet orice probleme ProGuard, este bine să configurați o versiune separată de depanare minificată ca aceasta:

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

cu acest tip de construcție, veți putea conecta depanatorul, puteți rula teste UI (de asemenea pe un server CI) sau puteți testa aplicația pentru posibile probleme pe o versiune cât mai apropiată de versiunea dvs.

concluzie: Când utilizați ProGuard ar trebui să întotdeauna QA eliberarea dvs. construiește bine, fie prin teste end-to-end sau manual trece prin toate ecranele din aplicația pentru a vedea dacă ceva lipsește sau crashing.

Runtime adnotări, tip introspecție

ProGuard va elimina în mod implicit toate adnotările și chiar unele informații de tip surplus din Codul. Pentru unele biblioteci, aceasta nu este o problemă — cele care procesează adnotări și generează cod la momentul compilării (cum ar fi Dagger 2 sau Glide și multe altele) ar putea să nu aibă nevoie de aceste adnotări mai târziu când programul rulează.

există o altă clasă de instrumente care inspectează efectiv adnotările sau analizează informațiile de tip ale parametrilor și excepțiilor în timpul rulării. Retrofit, de exemplu, face acest lucru prin interceptarea apelurilor metoda utilizând un Proxy obiect, apoi uita la adnotări și informații de tip pentru a decide ce să pună sau să citească din cererea HTTP.

concluzie: uneori este necesar să se păstreze informațiile de tip și adnotările care sunt citite în timpul rulării, spre deosebire de timpul de compilare. Puteți consulta lista de atribute din manualul ProGuard.

-keepattributes *Annotation*, Signature, Exception

dacă utilizați fișierul de configurare Android ProGuard implicit (getDefaultProguardFile('proguard-android.txt')), primele două opțiuni — adnotări și semnătură — sunt specificate pentru dvs. Dacă nu utilizați valoarea implicită, trebuie să vă asigurați că le adăugați singur (de asemenea, nu strică să le duplicați doar dacă știți că sunt o cerință pentru aplicația dvs.).

mutarea totul la pachetul implicit

-repackageclasses opțiune nu este adăugată în mod implicit în config ProGuard. Dacă vă ascundeți deja codul și ați rezolvat probleme cu regulile de păstrare corespunzătoare, puteți adăuga această opțiune pentru a reduce și mai mult dimensiunea DEX. Funcționează mutând toate clasele la pachetul implicit (rădăcină), eliberând în esență spațiul ocupat de șiruri de caractere precum „com.exemplu.myapp.somepackage”.

-repackageclasses

optimizări ProGuard

după cum am menționat mai devreme, ProGuard poate face 3 lucruri pentru tine:

  1. scapă de codul neutilizat,
  2. redenumește identificatorii pentru a face Codul mai mic,
  3. efectuează optimizări întregi ale programului.

așa cum o văd eu, toată lumea ar trebui să încerce și să configureze construi lor pentru a obține 1. și 2. lucrez.

pentru a debloca 3. (optimizări suplimentare), trebuie să utilizați un alt fișier de configurare ProGuard implicit. Schimbați parametrul proguard-android.txt la proguard-android-optimize.txt în build.gradle:

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

Acest lucru va face ca lansarea dvs. să fie mai lentă, dar va face ca aplicația dvs. să ruleze mai repede și să reducă și mai mult dimensiunea codului, mulțumesc pentru optimizări, cum ar fi metoda încăptușirea, îmbinarea clasei și eliminarea mai agresivă a codului. Fiți pregătit totuși, că s-ar putea introduce bug-uri noi și dificil de diagnosticat, deci folosiți-l cu prudență și dacă ceva nu funcționează, asigurați-vă că dezactivați anumite optimizări sau dezactivați cu totul utilizarea configurației de optimizare.

în cazul Plaid, optimizările ProGuard au interferat cu modul în care Retrofit folosește obiecte Proxy fără implementări concrete și au eliminat câțiva parametri ai metodei care erau de fapt necesari. A trebuit să adaug această linie la config:

-optimizations !method/removal/parameter

puteți găsi o listă de optimizări posibile și cum să le dezactivați în manualul ProGuard.

când să utilizați @ Keep și-keep

@Keepsuportul este implementat de fapt ca o grămadă de-keep reguli în fișierul implicit de reguli Android ProGuard, deci sunt în esență echivalente. Specificarea-keep reguli este mai flexibil, deoarece oferă metacaractere, puteți utiliza, de asemenea, diferite variante care fac lucruri ușor diferite (-keepnames-keepclasseswithmembers și mai mult).

ori de câte ori este necesară o regulă simplă „păstrați această clasă” sau „păstrați această metodă”, prefer de fapt simplitatea adăugării unui@Keep adnotare pe clasă sau membru, deoarece rămâne aproape de cod, aproape ca documentația.

dacă un alt dezvoltator care vine după mine vrea să refactorizeze Codul, vor ști imediat că o clasă/membru marcat cu@Keep necesită o manipulare specială, fără a fi nevoie să vă amintiți să consultați configurația ProGuard și să riscați să spargeți ceva. De asemenea, majoritatea refactorărilor de cod din IDE ar trebui să păstreze automat adnotarea @Keep cu clasa.

statistici Plaid

iată câteva statistici din Plaid, care arată cât de mult cod am reușit să elimin folosind ProGuard. Pe o aplicație mai complexă, cu mai multe dependențe și un DEX mai mare, economiile pot fi și mai substanțiale.