實(shí)用 ProGuard 規(guī)則示例

我在之前的文章中解釋了 為什么每個(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 ButcherPlaid.

從 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 將你的字段重命名為 ab,所以一切看起來似乎都有效农猬,數(shù)據(jù)也會(huì)被正確地保存和加載扑毡。

然而,當(dāng)你再一次構(gòu)建你的應(yīng)用并發(fā)布版本 N+1 的應(yīng)用時(shí)盛险,ProGuard 可能會(huì)決定將你的字段重命名為某些其他的,比如 cd勋又。因此苦掘,之前保存的數(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 / getResourceClassLoader.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ī)則呢赤套?
我只能給你一些提示:

  1. 閱讀構(gòu)建輸出和 logcat!
  2. 構(gòu)建警告會(huì)告訴你添加哪些 -dontwarn 規(guī)則
  3. ClassNotFoundException珊膜、MethodNotFoundExceptionFieldNotFoundException 會(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í)處理注解與生成代碼的庫(例如 Dagger2Glide 等等)可能以后程序運(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 可以為你做三件事:

  1. 它擺脫了未使用的代碼雨席,
  2. 重命名標(biāo)識(shí)符從而使代碼更小菩咨,
  3. 對(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é)省的可能更多缩麸。

image.png

如果發(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ì)劃抖甘、官方微博热鞍、知乎專欄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末衔彻,一起剝皮案震驚了整個(gè)濱河市薇宠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌艰额,老刑警劉巖澄港,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異柄沮,居然都是意外死亡回梧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門祖搓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狱意,“玉大人,你說我怎么就攤上這事棕硫∷柩模” “怎么了袒啼?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵哈扮,是天一觀的道長(zhǎng)纬纪。 經(jīng)常有香客問我,道長(zhǎng)滑肉,這世上最難降的妖魔是什么包各? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮靶庙,結(jié)果婚禮上问畅,老公的妹妹穿的比我還像新娘。我一直安慰自己六荒,他們只是感情好护姆,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掏击,像睡著了一般卵皂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上砚亭,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天灯变,我揣著相機(jī)與錄音,去河邊找鬼捅膘。 笑死添祸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的寻仗。 我是一名探鬼主播刃泌,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼愧沟!你這毒婦竟也來了蔬咬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤沐寺,失蹤者是張志新(化名)和其女友劉穎林艘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體混坞,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狐援,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了究孕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啥酱。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖厨诸,靈堂內(nèi)的尸體忽然破棺而出镶殷,到底是詐尸還是另有隱情,我是刑警寧澤微酬,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布绘趋,位于F島的核電站颤陶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏陷遮。R本人自食惡果不足惜滓走,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望帽馋。 院中可真熱鬧搅方,春花似錦、人聲如沸绽族。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绣溜。三九已至,卻和暖如春娄蔼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锚沸。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留距潘,地道東北人音比。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓洞翩,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親焰望。 傳聞我的和親對(duì)象是個(gè)殘疾皇子骚亿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容