Articles

gyakorlati ProGuard szabályok példák

Wojtek Kalici Xhamski
Wojtek Kalici ons

kövesse
Feb 20, 2018 · 9 min olvasni

az előző cikkemben elmagyaráztam, hogy miért kell mindenkinek használnia a ProGuard-ot Android-alkalmazásaihoz, hogyan engedélyezheti azt, és milyen hibákkal találkozhat. Sok elmélet volt benne, mivel úgy gondolom, hogy fontos megérteni az alapelveket, hogy felkészüljünk az esetleges problémák kezelésére.

külön cikkben is beszéltem a ProGuard azonnali Alkalmazásépítéshez történő konfigurálásának nagyon specifikus problémájáról.

ebben a részben szeretnék beszélni a ProGuard szabályok gyakorlati példáiról egy közepes méretű mintaalkalmazáson: Plaid Nick Butcher.

a Plaid valójában nagyszerű témának bizonyult a ProGuard problémák kutatásához, mivel olyan 3rd party könyvtárak keverékét tartalmazza, amelyek olyan dolgokat használnak, mint a kommentárfeldolgozás és a kódgenerálás, a reflexió, a java erőforrás betöltése és a natív kód (jni). Kivontam és feljegyeztem néhány gyakorlati tanácsot, amelyek általában más alkalmazásokra vonatkoznak:

Adatosztályok

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

valószínűleg minden alkalmazásnak van valamilyen adatosztálya (más néven DMOs, modellek stb. a kontextustól és az alkalmazás architektúrájának helyétől függően). Az adatobjektumokkal kapcsolatban az a helyzet, hogy általában egy bizonyos ponton betöltik vagy mentik (sorosítják) valamilyen más adathordozóra, például hálózatra (HTTP kérés), adatbázisra (ORM-en keresztül), JSON fájlra a lemezen vagy egy Firebase adattárolóban.

sok olyan eszköz, amely egyszerűsíti a mezők sorosítását és deserializálását, reflexióra támaszkodik. GSON, Retrofit, Firebase – mindegyik megvizsgálja az adatosztályok mezőneveit, és átalakítja őket egy másik reprezentációvá (például: {"name”: "Sue”, "age”: 28}), akár szállítás, akár tárolás céljából. Ugyanez történik, amikor adatokat olvasnak egy Java objektumba — látnak egy kulcs-érték párost "name”:”John” és megpróbálják alkalmazni egy Java objektumra a String name mezőben.

következtetés: nem hagyhatjuk, hogy a ProGuard átnevezze vagy eltávolítsa a mezőket ezeken az adatosztályokon, mivel meg kell egyezniük a sorosított formátummal. Ez egy biztos tét, hogy adjunk egy @Keep kommentár az egész osztály vagy helyettesítő szabály minden modell:

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

figyelmeztetés: Lehetséges, hogy hibát követ el annak tesztelésekor, hogy az alkalmazás érzékeny-e erre a problémára. Ha például egy objektumot JSON-ra szerializál, és lemezre menti az alkalmazás N verziójában a megfelelő keep szabályok nélkül, a mentett adatok így nézhetnek ki: {"a”: "Sue”, "b”: 28}. Mivel a ProGuard átnevezte a mezőket a és b névre, úgy tűnik, hogy minden működik, az adatok mentésre és betöltésre kerülnek.

amikor azonban újra elkészíti az alkalmazást, és kiadja az alkalmazás N+1 verzióját, a ProGuard dönthet úgy, hogy átnevezi a mezőket valami másra, például cés d. Ennek eredményeként a korábban mentett adatok nem töltődnek be.

meg kell győződnie arról, hogy a megfelelő keep szabályok Az első helyen.

A natív oldalról hívott Java kód (JNI)

az Android alapértelmezett ProGuard fájljai (mindig fel kell venni őket, van néhány igazán hasznos szabályuk) már tartalmaznak egy szabályt a natív oldalon megvalósított módszerekre (-keepclasseswithmembernames class * { native <methods>; }). Sajnos nincs mindenre kiterjedő módja annak, hogy a kódot az ellenkező irányba hívhassuk: a JNI-től a Java-ig.

a JNI-vel teljesen lehetséges egy JVM objektum felépítése, vagy egy metódus megkeresése és meghívása egy JVM fogantyún a C/C++ kódból, és valójában a Plaid-ban használt könyvtárak egyike ezt teszi.

következtetés: mivel a ProGuard csak a Java osztályokat tudja megvizsgálni, nem fog tudni a natív kódban előforduló felhasználásokról. Kifejezetten meg kell őriznünk az osztályok és tagok ilyen használatát egy @Keep annotáció vagy -keep szabály segítségével.

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

erőforrások megnyitása a JAR/APK-ból

az Androidnak saját erőforrásai és eszközei vannak, amelyek általában nem jelenthetnek problémát a ProGuard számára. A sima Java – ban azonban van egy másik mechanizmus az erőforrások betöltésére közvetlenül egy JAR fájlból, és néhány harmadik féltől származó könyvtár akkor is használhatja, ha Android-alkalmazásokban fordítják le (ebben az esetben megpróbálják betölteni az APK-ból).

a probléma az, hogy ezek az osztályok általában a saját csomagnevük alatt keresnek erőforrásokat (ami a JAR vagy az APK fájl elérési útját jelenti). A ProGuard képes átnevezni a csomagneveket, amikor elfedődik, így a fordítás után előfordulhat, hogy az osztály és az erőforrás fájlja már nem ugyanabban a csomagban van a végső APK-ban.

a betöltő erőforrások ilyen módon történő azonosításához keresse meg a Class.getResourceAsStream / getResourceés ClassLoader.getResourceAsStream / getResource hívásokat a kódjában és bármely harmadik fél könyvtárában, amelytől függ.

következtetés: meg kell őriznünk minden olyan osztály nevét, amely ezzel a mechanizmussal tölti be az APK erőforrásait.

a Plaid-ban valójában kettő van — egy az OkHttp könyvtárban és egy a Jsoup-ban:

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

Hogyan hozzunk létre szabályokat harmadik fél könyvtáraihoz

egy ideális világban minden használt függőség megadná a szükséges ProGuard szabályokat az AAR-ban. Néha elfelejtik ezt megtenni, vagy csak üvegeket tesznek közzé, amelyeknek nincs szabványosított módja a ProGuard-szabályok biztosítására.

ebben az esetben, mielőtt elkezdené az alkalmazás hibakeresését és a szabályok kidolgozását, ne felejtse el ellenőrizni a dokumentációt. Egyes könyvtári szerzők ajánlott ProGuard szabályokat kínálnak (például a kockás utólagos felszerelés), amelyek sok időt és frusztrációt takaríthatnak meg. Sajnos sok könyvtár nem (mint például a jsoup és a Bypass esetében ebben a cikkben). Vegye figyelembe azt is, hogy bizonyos esetekben a könyvtárhoz mellékelt konfiguráció csak letiltott optimalizálásokkal fog működni, így ha bekapcsolja őket, akkor feltérképezetlen területen lehet.

tehát hogyan kell kitalálni a szabályokat, ha a könyvtár nem biztosítja őket?
csak néhány mutatót tudok adni:

  1. olvassa el a build kimenetet és a logcat-ot!
  2. A Build figyelmeztetések megmondják, hogy melyik -dontwarn hozzáadandó szabályok
  3. ClassNotFoundExceptionMethodNotFoundException and FieldNotFoundException megmondja, hogy melyik -keep hozzáadandó szabályok

örülnie kell, ha az alkalmazás összeomlik, ha a ProGuard engedélyezve van — valahol meg kell kezdenie a vizsgálatot 🙂

a hibakeresési problémák legrosszabb osztálya az, amikor az alkalmazás működik, de például nem jelenít meg képernyőt, vagy nem tölt be adatokat a hálózatról.

Ez az, ahol meg kell fontolnia néhány forgatókönyvet, amelyet ebben a cikkben leírtam, és be kell piszkolnia a kezét, még a harmadik fél kódjába is belemerülve, és megértve, hogy miért bukhat el, például amikor reflexiót, önvizsgálatot vagy JNI-t használ.

hibakeresés és verem nyomok

a ProGuard alapértelmezés szerint eltávolít sok olyan kódattribútumot és rejtett metaadatot, amelyek nem szükségesek a program végrehajtásához . Ezek közül néhány valóban hasznos a fejlesztő számára — például érdemes megőrizni a forrásfájlneveket és a sorszámokat a verem nyomaihoz, hogy megkönnyítse a hibakeresést:

-keepattributes SourceFile, LineNumberTable

ne felejtse el elmenteni a ProGuard leképezési fájlokat, amikor egy kiadási verziót készít, és feltölti őket Lejátszásra, hogy elhomályosítsa a verem nyomait az Ön által tapasztalt összeomlásokból felhasználók.

ha hibakeresőt szeretne csatolni a metóduskód lépéséhez az alkalmazás ProGuarded buildjében, akkor a következő attribútumokat is meg kell őriznie a helyi változókkal kapcsolatos hibakeresési információk megőrzéséhez (csak erre a sorra van szüksége a debug build típusában):

-keepattributes LocalVariableTable, LocalVariableTypeTable

minified debug Build Type

Az alapértelmezett build típusok úgy vannak konfigurálva, hogy a debug ne futtassa a ProGuard programot. Ennek van értelme, mert a fejlesztés során gyorsan akarunk iterálni és lefordítani, de továbbra is azt akarjuk, hogy a kiadás buildje a ProGuard-ot használja, hogy a lehető legkisebb és optimalizált legyen.

de a ProGuard-problémák teljes teszteléséhez és hibakereséséhez jó, ha külön, minified debug build-et állít be, mint ez:

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

ezzel a build típussal csatlakoztathatja a hibakeresőt, futtathatja az UI teszteket (CI szerveren is), vagy monkey tesztelheti az alkalmazást a lehetséges problémákra egy olyan builden, amely a lehető legközelebb van a kiadás buildjéhez.

következtetés: Amikor ProGuard mindig QA a kiadás épít alaposan, akár azáltal, end-to-end tesztek vagy manuálisan megy át az összes képernyőn az app, hogy ha valami hiányzik, vagy összeomlik.

futásidejű megjegyzések, írja be az introspection

ProGuard alapértelmezés szerint eltávolítja az összes kommentárt, sőt néhány felesleges típusú információt a kódból. Néhány könyvtár esetében ez nem jelent problémát — azoknak, amelyek kommentárokat dolgoznak fel és kódot generálnak fordításkor (például Dagger 2 vagy Glide és még sok más), lehet, hogy később nem lesz szükségük ezekre a megjegyzésekre, amikor a program fut.

van egy másik eszközosztály, amely ténylegesen megvizsgálja a megjegyzéseket, vagy megvizsgálja a paraméterek és kivételek típusinformációit futásidőben. A Retrofit például ezt úgy teszi meg, hogy a metódushívásokat egy Proxy objektum használatával elfogja, majd megnézi a kommentárokat és beírja az információkat, hogy eldöntse, mit tegyen vagy olvasson a HTTP kérésből.

következtetés: néha meg kell őrizni a futási időben olvasott típusinformációkat és kommentárokat, szemben a fordítási idővel. Az attribútumok listáját a ProGuard kézikönyvben tekintheti meg.

-keepattributes *Annotation*, Signature, Exception

Ha az alapértelmezett Android ProGuard konfigurációs fájlt használja (getDefaultProguardFile('proguard-android.txt')), akkor az első két lehetőség — Megjegyzések és aláírás — meg van adva az Ön számára. Ha nem az alapértelmezett értéket használja, akkor feltétlenül saját maga kell hozzáadnia őket (az sem árt, ha csak lemásolja őket, ha tudja, hogy ezek az alkalmazás követelményei).

minden áthelyezése Az alapértelmezett csomagba

a -repackageclasses opció alapértelmezés szerint nincs hozzáadva a ProGuard konfigurációban. Ha már elhomályosította a kódját, és bármilyen problémát megoldott a megfelelő keep szabályokkal, hozzáadhatja ezt a lehetőséget a DEX méretének további csökkentése érdekében. Úgy működik, hogy az összes osztályt az alapértelmezett (gyökér) csomagba helyezi, lényegében felszabadítva a húrok által elfoglalt helyet, mint például a “com.példa.az appot.somepackage”.

-repackageclasses

ProGuard optimalizálás

mint már említettem, a ProGuard 3 dolgot tehet az Ön számára:

  1. megszabadul a fel nem használt kódtól,
  2. átnevezi az azonosítókat, hogy a kód kisebb legyen,
  3. teljes programoptimalizálást hajt végre.

ahogy én látom, mindenkinek meg kell próbálnia konfigurálni a buildjét, hogy 1-et kapjon. és 2. dolgozom.

feloldásához 3. (további optimalizálás), egy másik alapértelmezett ProGuard konfigurációs fájlt kell használnia. Változtassa meg a proguard-android.txt paramétert proguard-android-optimize.txt a build.gradle:

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

Ez lassítja a kiadás felépítését, de potenciálisan gyorsabbá teszi az alkalmazást, és még tovább csökkenti a kód méretét, köszönhetően az optimalizáláshoz, mint például a módszer beillesztése, az osztály egyesítése és az agresszívebb kód eltávolítása. Készüljön fel azonban arra, hogy új és nehezen diagnosztizálható hibákat vezethet be, ezért óvatosan használja, és ha valami nem működik, mindenképpen tiltson le bizonyos optimalizálásokat, vagy tiltsa le teljesen az optimalizáló konfiguráció használatát.

a Plaid esetében a ProGuard optimalizációja megzavarta, hogy a Retrofit hogyan használ Proxy objektumokat konkrét implementációk nélkül, és eltávolított néhány metódusparamétert, amelyek valóban szükségesek voltak. Ezt a sort hozzá kellett adnom a konfigurációmhoz:

-optimizations !method/removal/parameter

a lehetséges optimalizálások listáját és azok letiltásának módját a ProGuard kézikönyvben találja.

mikor kell használni @Keep and-keep

@KeepA támogatás valójában egy csomó-keep szabályok Az alapértelmezett Android ProGuard szabályfájlban, tehát lényegében egyenértékűek. A -keep szabályok megadása rugalmasabb, mivel helyettesítő karaktereket kínál, különböző változatokat is használhat, amelyek kissé eltérő dolgokat végeznek (-keepnames-keepclasseswithmembers és így tovább).

amikor egy egyszerű “keep this class” vagy”keep this method”szabályra van szükség, valójában inkább a@Keep annotáció hozzáadásának egyszerűségét részesítem előnyben az osztályon vagy tagon, mivel közel marad a kódhoz, majdnem olyan, mint a dokumentáció.

ha egy másik fejlesztő, aki utánam jön, újra akarja írni a kódot, azonnal tudni fogják, hogy a @Keep jelöléssel ellátott osztály/tag speciális kezelést igényel, anélkül, hogy emlékeznie kellene a ProGuard konfigurációjára, és kockáztatnia kellene, hogy valamit megtör. Szintén a legtöbb kód refactorings az IDE meg kell őriznie a @Keep annotáció az osztály automatikusan.

Plaid stats

íme néhány statisztika a Plaid-tól, amelyek megmutatják, hogy mennyi kódot sikerült eltávolítanom a ProGuard használatával. Egy összetettebb, több függőséggel és nagyobb DEX-szel rendelkező alkalmazás esetén a megtakarítás még jelentősebb lehet.