我在之前的文章中解釋了 為什么每個(gè)人都應(yīng)該將 ProGuard 用于他們的 Android 應(yīng)用馋没、怎么啟用它以及在使用中可能面臨的錯(cuò)誤種類稠氮。這其中涉及很多理論,因?yàn)槲艺J(rèn)為理解基本原理以準(zhǔn)備好處理任何潛在問題非常重要。
我還在一篇單獨(dú)的文章中談到了 為 Instant App 構(gòu)建配置 ProGuard 的非常具體的問題族操。
在這里零渐,我想談 ProGuard 規(guī)則在中型樣例應(yīng)用上的實(shí)用示例:出自 Nick Butcher 的 Plaid.
從 Plaid 中吸取的教訓(xùn)
Plaid 實(shí)際上是研究 ProGuard 問題的一個(gè)很好的主題嘶伟,因?yàn)樗褂米⒔馓幚砼c代碼生成工腋、反射、Java資源加載和原生代碼(JNI)的第三方庫的混合體屿衅。我提取并記錄下了一些適用于其他應(yīng)用的實(shí)用建議:
數(shù)據(jù)類
public class User {
String name;
int age;
...
}
每個(gè)應(yīng)用可能都有某種數(shù)據(jù)類(也被稱為 DMOs埃难,模型等,取決于上下文以及它們處在應(yīng)用架構(gòu)中的位置)。關(guān)于數(shù)據(jù)對(duì)象的事實(shí)是涡尘,通常在某些時(shí)候他們將被加載或保存(序列化)到某些其他介質(zhì)中忍弛,例如網(wǎng)絡(luò)(HTTP 請(qǐng)求)、數(shù)據(jù)庫(通過 ORM)考抄、磁盤上的 JSON 文件或 Firebase 數(shù)據(jù)存儲(chǔ)细疚。
許多簡(jiǎn)化序列化與反序列化這些字段的工具依賴于反射。GSON川梅、Retrofit疯兼、Firebase —— 他們都檢查數(shù)據(jù)類的字段名并把它們轉(zhuǎn)換成另一種表現(xiàn)形式(例如:{“name”: “Sue”, “age”: 28}
),用于傳輸或存儲(chǔ)贫途。它們將數(shù)據(jù)讀入 Java 對(duì)象時(shí)也是同理 —— 它們看到鍵值對(duì) “name”:”John”
并嘗試通過查找 String name
字段將其應(yīng)用到 Java 對(duì)象上吧彪。
結(jié)論:我們不能讓 ProGuard 重命名或刪除這些數(shù)據(jù)類的任何字段,因?yàn)樗鼈儽仨毰c序列化的格式匹配丢早。最好給整個(gè)類添加一個(gè) @Keep
注解或者給所有模型添加通配符規(guī)則:
-keep class io.plaidapp.data.api.dribbble.model.** { *; }
警告:在測(cè)試你的應(yīng)用是否容易受到這個(gè)問題的影響是可能會(huì)出錯(cuò)姨裸。例如,如果你在版本 N 的應(yīng)用程序中將一個(gè)對(duì)象序列化成 JSON 并將其保存到磁盤而沒有使用適當(dāng)?shù)?keep 規(guī)則怨酝,那么保存的數(shù)據(jù)可能看起來像這樣:
{“a”: “Sue”, “b”: 28}
傀缩。因?yàn)?ProGuard 將你的字段重命名為a
和b
,所以一切看起來似乎都有效农猬,數(shù)據(jù)也會(huì)被正確地保存和加載扑毡。
然而,當(dāng)你再一次構(gòu)建你的應(yīng)用并發(fā)布版本 N+1 的應(yīng)用時(shí)盛险,ProGuard 可能會(huì)決定將你的字段重命名為某些其他的,比如
c
和d
勋又。因此苦掘,之前保存的數(shù)據(jù)將無法加載。
首先你必須確保你有適當(dāng)?shù)?keep 規(guī)則楔壤。
從原生層調(diào)用的 Java 代碼(JNI)
Android 的 默認(rèn) ProGuard 文件(你應(yīng)該總是包括它們鹤啡,它們有一些非常有用的規(guī)則)已經(jīng)包含了針對(duì)在原生層實(shí)現(xiàn)的方法的規(guī)則(-keepclasseswithmembernames class * { native <methods>; }
)。遺憾的是蹲嚣,沒有一種全能的方法可以保留從反方向調(diào)用的代碼:從 JNI 到 Java递瑰。
利用 JNI,完全有可能從 C / C++ 代碼中構(gòu)造 JVM 對(duì)象或者找到并調(diào)用 JVM 句柄的方法隙畜,而且事實(shí)上抖部,Plaid 的一個(gè)庫就是這樣。
結(jié)論:因?yàn)?ProGuard 只能審查 Java 類议惰,所以它不會(huì)知道任何在原生代碼中發(fā)生的使用慎颗。我們必須通過 @Keep
注解或 -keep
規(guī)則來顯式地保留這些類和成員的使用。
-keep, includedescriptorclasses
class in.uncod.android.bypass.Document { *; }
-keep, includedescriptorclasses
class in.uncod.android.bypass.Element { *; }
從 JAR/APK 打開資源
Android 有其自己的資源系統(tǒng),通常不會(huì)有 ProGuard 的問題俯萎。然而傲宜,在普通的 Java 中有另一種 直接從 JAR 文件加載資源的機(jī)制。并且某些第三方庫即使被編譯到 Android 應(yīng)用中也可能會(huì)使用這種機(jī)制(在這種情況下夫啊,它們將嘗試從 APK 加載)函卒。
問題是通常這些類會(huì)在自己的包名下尋找資源(這將轉(zhuǎn)換為 JAR 或 APK 中的文件路徑)。ProGuard 可能在混淆時(shí)重命名包名撇眯,因此在編譯之后可能會(huì)發(fā)生類及其資源文件不再位于最終 APK 中的同一包內(nèi)报嵌。
要以這種方式識(shí)別加載資源,你可以在你的代碼和任何你依賴的第三方庫中查找 Class.getResourceAsStream / getResource
和 ClassLoader.getResourceAsStream / getResource
的調(diào)用叛本。
結(jié)論:我們應(yīng)該保留任何使用這種機(jī)制從 APK 加載資源的類的名字沪蓬。
在 Plaid 中,實(shí)際上有兩個(gè) —— 一個(gè)在 OKHttp 庫中来候,另一個(gè)在 Jsoup 庫中:
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-keepnames class org.jsoup.nodes.Entities
如何為第三方庫制定規(guī)則
在理想的世界里跷叉,每個(gè)你使用的依賴都會(huì)在 AAR 中提供他們所需要的 ProGuard 規(guī)則。有時(shí)他們會(huì)忘記這樣做或只發(fā)布 JAR营搅,這些 JAR 沒有標(biāo)準(zhǔn)的方式來提供 ProGuard 規(guī)則云挟。
在這種情況下,在開始調(diào)試應(yīng)用和制定規(guī)則之前转质,記得查看文檔园欣。一些庫的作者提供推薦的 ProGuard 規(guī)則(例如在 Plaid 中使用的 Retrofit),這可以為你節(jié)省大量時(shí)間休蟹,并讓你免受挫折沸枯。遺憾的是,很多庫都不會(huì)這樣(例如這篇文章中提到的 Jsoup 和 Bypass 的情況)赂弓。另請(qǐng)注意绑榴,在某些情況下,隨庫提供的配置只能在禁用優(yōu)化的條件下起作用盈魁,因此如果你開啟了優(yōu)化翔怎,那么你可能踏入了未知領(lǐng)域。
那么當(dāng)庫沒有提供規(guī)則時(shí)杨耙,如何制定規(guī)則呢赤套?
我只能給你一些提示:
- 閱讀構(gòu)建輸出和 logcat!
- 構(gòu)建警告會(huì)告訴你添加哪些
-dontwarn
規(guī)則 -
ClassNotFoundException
珊膜、MethodNotFoundException
和FieldNotFoundException
會(huì)告訴你添加哪些-keep
規(guī)則
當(dāng)你使用了 ProGuard 的應(yīng)用崩潰時(shí)容握,你應(yīng)該慶幸 —— 你將有一個(gè)開始調(diào)查的地方 :)
最糟糕的一類調(diào)試問題是你的應(yīng)用工作了,但是例如屏幕沒有顯示或沒有從網(wǎng)絡(luò)加載數(shù)據(jù)车柠。
在這里你需要去考慮我在本文中描述的一些場(chǎng)景并動(dòng)手實(shí)踐唯沮,甚至扎入第三方庫的代碼中并理解它可能失敗的原因脖旱,例如當(dāng)它使用反射、攔截或 JNI 時(shí)介蛉。
調(diào)試與堆棧跟蹤
ProGuard 默認(rèn)會(huì)刪除程序執(zhí)行不需要的許多代碼屬性和隱藏元數(shù)據(jù)萌庆。其中一些對(duì)開發(fā)者實(shí)際上很有用 —— 例如,你可能希望保留堆棧跟蹤的源文件名和行號(hào)币旧,以使調(diào)試更容易:
-keepattributes SourceFile, LineNumberTable
你也應(yīng)當(dāng)記得 保存構(gòu)建發(fā)行版本時(shí)生成的 ProGuard 映射文件并將其上傳到 Play 以便從用戶遇到的任何崩潰中得到反混淆的堆棧跟蹤践险。
如果要在使用 ProGuard 構(gòu)建的應(yīng)用中附加調(diào)試器來逐步執(zhí)行方法代碼,那么你還應(yīng)該保留以下屬性吹菱,以保留關(guān)于局部變量的一些調(diào)試信息(在 debug
構(gòu)建類型中只需要這一行):
-keepattributes LocalVariableTable, LocalVariableTypeTable
縮小的調(diào)試構(gòu)建類型
構(gòu)建類型的默認(rèn)配置為 debug 不使用 ProGuard巍虫。這很有道理缓窜,因?yàn)槲覀兿M陂_發(fā)時(shí)快速迭代和編譯碱呼,但仍然希望使用 ProGuard 來構(gòu)建發(fā)布版本以使其盡可能小和優(yōu)化。
但是為了全面測(cè)試和調(diào)試任何 ProGuard 問題痘煤,最好像這樣設(shè)置一個(gè)單獨(dú)的输瓜、縮小的調(diào)試構(gòu)建:
buildTypes {
debugMini {
initWith debug
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
matchingFallbacks = ['debug']
}
}
使用這種構(gòu)建類型瓦胎,你將能夠 連接調(diào)試器, 運(yùn)行 UI 測(cè)試 (也在持續(xù)集成服務(wù)器上) 或 monkey 測(cè)試 你的應(yīng)用,以便在盡可能接近發(fā)布版本的構(gòu)建上發(fā)現(xiàn)可能的問題尤揣。
結(jié)論:當(dāng)你使用 ProGuard 時(shí)搔啊,你應(yīng)當(dāng)總是通過端到端測(cè)試,或者手動(dòng)瀏覽應(yīng)用的所有頁面來看是否有任何缺失或崩潰北戏,以對(duì)你的構(gòu)建版本進(jìn)行徹底的 QA负芋。
運(yùn)行時(shí)注解,類型攔截
ProGuard 默認(rèn)會(huì)刪除代碼中的所有注解甚至一些剩余的類型信息嗜愈。對(duì)于一些庫來說旧蛾,這不是個(gè)問題 —— 那些在編譯時(shí)處理注解與生成代碼的庫(例如 Dagger2 或 Glide 等等)可能以后程序運(yùn)行時(shí)不需要這些注解。
還有另外一類實(shí)際上在運(yùn)行時(shí)檢查注解或查看參數(shù)與異常的類型信息的工具蠕嫁。例如 Retrofit 就這樣做锨天,通過使用 Proxy
對(duì)象來攔截方法調(diào)用,然后查看注解和類型信息來決定什么內(nèi)容該放入 HTTP 請(qǐng)求或從 HTTP 請(qǐng)求中讀取拌阴。
結(jié)論:有時(shí)需要并保留在運(yùn)行時(shí)而不是編譯時(shí)被取的類型信息與注解。你可以查看 ProGuard 手冊(cè)中的屬性列表奶镶。
-keepattributes *Annotation*, Signature, Exception
如果你使用默認(rèn)的Android ProGuard 配置文件(
getDefaultProguardFile('proguard-android.txt')
)迟赃,那么前兩個(gè)選項(xiàng) —— 注解和簽名 —— 是專門為你準(zhǔn)備的。如果你沒有使用默認(rèn)的配置文件厂镇,那么你必須保證你自己添加它們(如果你知道你的應(yīng)用需要他們纤壁,那么重復(fù)它們也沒有什么壞處)。
將所有內(nèi)容移至默認(rèn)包
默認(rèn)情況下捺信,ProGuard 配置中不會(huì)添加 -repackageclasses
選項(xiàng)酌媒。如果你已經(jīng)在混淆你的代碼并且使用適當(dāng)?shù)?keep 規(guī)則解決了任何問題欠痴,那么你可以添加這個(gè)選項(xiàng)以進(jìn)一步減小 DEX 的大小。它的工作原理是將所有類移至默認(rèn)(根)包秒咨,從而實(shí)質(zhì)上釋放了被像 「com.example.myapp.somepackage」這樣的字符串所占用的空間喇辽。
-repackageclasses
ProGuard 優(yōu)化
正如我之前提到的,ProGuard 可以為你做三件事:
- 它擺脫了未使用的代碼雨席,
- 重命名標(biāo)識(shí)符從而使代碼更小菩咨,
- 對(duì)整個(gè)程序進(jìn)行優(yōu)化。
在我看來陡厘,每個(gè)人都應(yīng)該嘗試并配置他們的構(gòu)建來使1. 和 2. 工作抽米。
為了解鎖 3.(額外的優(yōu)化),你必須使用其他默認(rèn)的 ProGuard 配置文件糙置。在你的 build.gradle
中云茸,將 proguard-android.txt
參數(shù)改為 proguard-android-optimize.txt
:
release {
minifyEnabled true
proguardFiles
getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
這會(huì)是你的發(fā)布構(gòu)建更慢,但可能會(huì)讓你的應(yīng)用運(yùn)行地更快和進(jìn)一步縮小代碼體積谤饭,這要?dú)w功于方法內(nèi)聯(lián)标捺、類合并與更侵略性的代碼刪除等優(yōu)化。但要做好準(zhǔn)備网持,它可能會(huì)引入新的宜岛、更難診斷的錯(cuò)誤,因此謹(jǐn)慎使用功舀,如果有任何不起作用萍倡,務(wù)必禁用某些特定的優(yōu)化或完全禁用優(yōu)化配置。
就 Plaid 來說辟汰,ProGuard 優(yōu)化干擾了 Retrofit 如何使用沒有具體實(shí)現(xiàn)的代理對(duì)象列敲,并剝離了一些實(shí)際需要的方法參數(shù)。我必須在我的配置中添加這一行:
-optimizations !method/removal/parameter
你可以在 ProGuard 中找到 可能的優(yōu)化列表以及如何禁用它們帖汞。
何時(shí)使用 @Keep
和 -keep
@Keep
的支持在默認(rèn)的 Android ProGuard 規(guī)則文件中實(shí)際上是通過一系列 -keep
規(guī)則實(shí)現(xiàn)的戴而,因此它們基本上是等效的。指定 -keep
規(guī)則更靈活翩蘸,因?yàn)樗峁┩ㄅ浞猓阋部梢允褂貌煌淖凅w,這些變體稍有不同(-keepnames
催首、-keepclasseswithmembers
以及更多)扶踊。
每當(dāng)需要一個(gè)簡(jiǎn)單的「保留這個(gè)類」或「保留這個(gè)方法」規(guī)則時(shí),我實(shí)際上更喜歡在類或成員上添加 @Keep
注解的簡(jiǎn)單性郎任,因?yàn)樗x代碼很近秧耗,幾乎就像文檔一樣。
如果其他開發(fā)者想要在我之后重構(gòu)代碼舶治,他們會(huì)立即知道被 @Keep
標(biāo)記的類 / 成員需要特殊處理分井,而不必記住和參考 ProGuard 配置并且冒著破壞某些東西的風(fēng)險(xiǎn)车猬。IDE 中大部分的代碼重構(gòu)也應(yīng)當(dāng)自動(dòng)保留類的 @Keep
注解。
Plaid 統(tǒng)計(jì)信息
這有一些來自 Plaid 的統(tǒng)計(jì)信息尺锚,它們展示了我通過使用 ProGuard 刪除了多少代碼珠闰。在有更多依賴和更大 DEX 的更復(fù)雜的應(yīng)用上,節(jié)省的可能更多缩麸。
如果發(fā)現(xiàn)譯文存在錯(cuò)誤或其他需要改進(jìn)的地方铸磅,歡迎到 掘金翻譯計(jì)劃 對(duì)譯文進(jìn)行修改并 PR,也可獲得相應(yīng)獎(jiǎng)勵(lì)積分杭朱。文章開頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接阅仔。
掘金翻譯計(jì)劃 是一個(gè)翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術(shù)文章的社區(qū),文章來源為 掘金 上的英文分享文章弧械。內(nèi)容覆蓋 Android八酒、iOS、前端刃唐、后端羞迷、區(qū)塊鏈、產(chǎn)品画饥、設(shè)計(jì)衔瓮、人工智能等領(lǐng)域,想要查看更多優(yōu)質(zhì)譯文請(qǐng)持續(xù)關(guān)注 掘金翻譯計(jì)劃抖甘、官方微博热鞍、知乎專欄。