[譯] 實用 ProGuard 規則示例

我在以前的文章中解釋了 爲何每一個人都應該將 ProGuard 用於他們的 Android 應用、怎麼啓用它以及在使用中可能面臨的錯誤種類。這其中涉及不少理論,由於我認爲理解基本原理以準備好處理任何潛在問題很是重要。html

我還在一篇單獨的文章中談到了 爲 Instant App 構建配置 ProGuard 的很是具體的問題。前端

在這裏,我想談 ProGuard 規則在中型樣例應用上的實用示例:出自 Nick ButcherPlaid.java

從 Plaid 中吸收的教訓

Plaid 其實是研究 ProGuard 問題的一個很好的主題,由於它包含使用註解處理與代碼生成、反射、Java資源加載和原生代碼(JNI)的第三方庫的混合體。我提取並記錄下了一些適用於其餘應用的實用建議:node

數據類

public class User {
  String name;
  int age;
  ...
}
複製代碼

每一個應用可能都有某種數據類(也被稱爲 DMOs,模型等,取決於上下文以及它們處在應用架構中的位置)。關於數據對象的事實是,一般在某些時候他們將被加載或保存(序列化)到某些其餘介質中,例如網絡(HTTP 請求)、數據庫(經過 ORM)、磁盤上的 JSON 文件或 Firebase 數據存儲。android

許多簡化序列化與反序列化這些字段的工具依賴於反射。GSON、Retrofit、Firebase —— 他們都檢查數據類的字段名並把它們轉換成另外一種表現形式(例如:{「name」: 「Sue」, 「age」: 28}),用於傳輸或存儲。它們將數據讀入 Java 對象時也是同理 —— 它們看到鍵值對 「name」:」John」 並嘗試經過查找 String name 字段將其應用到 Java 對象上。ios

結論:咱們不能讓 ProGuard 重命名或刪除這些數據類的任何字段,由於它們必須與序列化的格式匹配。最好給整個類添加一個 @Keep 註解或者給全部模型添加通配符規則:git

-keep class io.plaidapp.data.api.dribbble.model.** { *; }
複製代碼

警告:在測試你的應用是否容易受到這個問題的影響是可能會出錯。例如,若是你在版本 N 的應用程序中將一個對象序列化成 JSON 並將其保存到磁盤而沒有使用適當的 keep 規則,那麼保存的數據可能看起來像這樣:{「a」: 「Sue」, 「b」: 28}。由於 ProGuard 將你的字段重命名爲 ab,因此一切看起來彷佛都有效,數據也會被正確地保存和加載。github

然而,當你再一次構建你的應用併發布版本 N+1 的應用時,ProGuard 可能會決定將你的字段重命名爲某些其餘的,好比 cd。所以,以前保存的數據將沒法加載。數據庫

首先你必須確保你有適當的 keep 規則。後端

從原生層調用的 Java 代碼(JNI)

Android 的 默認 ProGuard 文件(你應該老是包括它們,它們有一些很是有用的規則)已經包含了針對在原生層實現的方法的規則(-keepclasseswithmembernames class * { native <methods>; })。遺憾的是,沒有一種全能的方法能夠保留從反方向調用的代碼:從 JNI 到 Java。

利用 JNI,徹底有可能從 C / C++ 代碼中構造 JVM 對象或者找到並調用 JVM 句柄的方法,並且事實上,Plaid 的一個庫就是這樣

結論:由於 ProGuard 只能審查 Java 類,因此它不會知道任何在原生代碼中發生的使用。咱們必須經過 @Keep 註解或 -keep 規則來顯式地保留這些類和成員的使用。

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

從 JAR/APK 打開資源

Android 有其本身的資源系統,一般不會有 ProGuard 的問題。然而,在普通的 Java 中有另外一種 直接從 JAR 文件加載資源的機制。而且某些第三方庫即便被編譯到 Android 應用中也可能會使用這種機制(在這種狀況下,它們將嘗試從 APK 加載)。

問題是一般這些類會在本身的包名下尋找資源(這將轉換爲 JAR 或 APK 中的文件路徑)。ProGuard 可能在混淆時重命名包名,所以在編譯以後可能會發生類及其資源文件再也不位於最終 APK 中的同一包內。

要以這種方式識別加載資源,你能夠在你的代碼和任何你依賴的第三方庫中查找 Class.getResourceAsStream / getResourceClassLoader.getResourceAsStream / getResource 的調用。

結論:咱們應該保留任何使用這種機制從 APK 加載資源的類的名字。

在 Plaid 中,實際上有兩個 —— 一個在 OKHttp 庫中,另外一個在 Jsoup 庫中:

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-keepnames class org.jsoup.nodes.Entities
複製代碼

如何爲第三方庫制定規則

在理想的世界裏,每一個你使用的依賴都會在 AAR 中提供他們所須要的 ProGuard 規則。有時他們會忘記這樣作或只發布 JAR,這些 JAR 沒有標準的方式來提供 ProGuard 規則。

在這種狀況下,在開始調試應用和制定規則以前,記得查看文檔。一些庫的做者提供推薦的 ProGuard 規則(例如在 Plaid 中使用的 Retrofit),這能夠爲你節省大量時間,並讓你免受挫折。遺憾的是,不少庫都不會這樣(例如這篇文章中提到的 Jsoup 和 Bypass 的狀況)。另請注意,在某些狀況下,隨庫提供的配置只能在禁用優化的條件下起做用,所以若是你開啓了優化,那麼你可能踏入了未知領域。

那麼當庫沒有提供規則時,如何制定規則呢? 我只能給你一些提示:

  1. 閱讀構建輸出和 logcat!
  2. 構建警告會告訴你添加哪些 -dontwarn 規則
  3. ClassNotFoundExceptionMethodNotFoundExceptionFieldNotFoundException 會告訴你添加哪些 -keep 規則

當你使用了 ProGuard 的應用崩潰時,你應該慶幸 —— 你將有一個開始調查的地方 :)

最糟糕的一類調試問題是你的應用工做了,可是例如屏幕沒有顯示或沒有從網絡加載數據。

在這裏你須要去考慮我在本文中描述的一些場景並動手實踐,甚至扎入第三方庫的代碼中並理解它可能失敗的緣由,例如當它使用反射、攔截或 JNI 時。

調試與堆棧跟蹤

ProGuard 默認會刪除程序執行不須要的許多代碼屬性和隱藏元數據。其中一些對開發者實際上頗有用 —— 例如,你可能但願保留堆棧跟蹤的源文件名和行號,以使調試更容易:

-keepattributes SourceFile, LineNumberTable
複製代碼

你也應當記得 保存構建發行版本時生成的 ProGuard 映射文件並將其上傳到 Play 以便從用戶遇到的任何崩潰中獲得反混淆的堆棧跟蹤。

若是要在使用 ProGuard 構建的應用中附加調試器來逐步執行方法代碼,那麼你還應該保留如下屬性,以保留關於局部變量的一些調試信息(在 debug 構建類型中只須要這一行):

-keepattributes LocalVariableTable, LocalVariableTypeTable
複製代碼

縮小的調試構建類型

構建類型的默認配置爲 debug 不使用 ProGuard。這頗有道理,由於咱們但願在開發時快速迭代和編譯,但仍然但願使用 ProGuard 來構建發佈版本以使其儘量小和優化。

可是爲了全面測試和調試任何 ProGuard 問題,最好像這樣設置一個單獨的、縮小的調試構建:

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

使用這種構建類型,你將可以 鏈接調試器, 運行 UI 測試 (也在持續集成服務器上) 或 monkey 測試 你的應用,以便在儘量接近發佈版本的構建上發現可能的問題。

結論:當你使用 ProGuard 時,你應當老是經過端到端測試,或者手動瀏覽應用的全部頁面來看是否有任何缺失或崩潰,以對你的構建版本進行完全的 QA。

運行時註解,類型攔截

ProGuard 默認會刪除代碼中的全部註解甚至一些剩餘的類型信息。對於一些庫來講,這不是個問題 —— 那些在編譯時處理註解與生成代碼的庫(例如 Dagger2Glide 等等)可能之後程序運行時不須要這些註解。

還有另一類實際上在運行時檢查註解或查看參數與異常的類型信息的工具。例如 Retrofit 就這樣作,經過使用 Proxy 對象來攔截方法調用,而後查看註解和類型信息來決定什麼內容該放入 HTTP 請求或從 HTTP 請求中讀取。

結論:有時須要並保留在運行時而不是編譯時被取的類型信息與註解。你能夠查看 ProGuard 手冊中的屬性列表

-keepattributes *Annotation*, Signature, Exception
複製代碼

若是你使用默認的Android ProGuard 配置文件(getDefaultProguardFile('proguard-android.txt')),那麼前兩個選項 —— 註解和簽名 —— 是專門爲你準備的。若是你沒有使用默認的配置文件,那麼你必須保證你本身添加它們(若是你知道你的應用須要他們,那麼重複它們也沒有什麼壞處)。

將全部內容移至默認包

默認狀況下,ProGuard 配置中不會添加 -repackageclasses 選項。若是你已經在混淆你的代碼而且使用適當的 keep 規則解決了任何問題,那麼你能夠添加這個選項以進一步減少 DEX 的大小。它的工做原理是將全部類移至默認(根)包,從而實質上釋放了被像 「com.example.myapp.somepackage」這樣的字符串所佔用的空間。

-repackageclasses
複製代碼

ProGuard 優化

正如我以前提到的,ProGuard 能夠爲你作三件事:

  1. 它擺脫了未使用的代碼,
  2. 重命名標識符從而使代碼更小,
  3. 對整個程序進行優化。

在我看來,每一個人都應該嘗試並配置他們的構建來使1. 和 2. 工做。

爲了解鎖 3.(額外的優化),你必須使用其餘默認的 ProGuard 配置文件。在你的 build.gradle 中,將 proguard-android.txt 參數改成 proguard-android-optimize.txt

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

這會是你的發佈構建更慢,但可能會讓你的應用運行地更快和進一步縮小代碼體積,這要歸功於方法內聯、類合併與更侵略性的代碼刪除等優化。但要作好準備,它可能會引入新的、更難診斷的錯誤,所以謹慎使用,若是有任何不起做用,務必禁用某些特定的優化或徹底禁用優化配置。

就 Plaid 來講,ProGuard 優化干擾了 Retrofit 如何使用沒有具體實現的代理對象,並剝離了一些實際須要的方法參數。我必須在個人配置中添加這一行:

-optimizations !method/removal/parameter
複製代碼

你能夠在 ProGuard 中找到 可能的優化列表以及如何禁用它們

什麼時候使用 @Keep-keep

@Keep 的支持在默認的 Android ProGuard 規則文件中其實是經過一系列 -keep 規則實現的,所以它們基本上是等效的。指定 -keep 規則更靈活,由於它提供通配符,你也可使用不一樣的變體,這些變體稍有不一樣(-keepnames-keepclasseswithmembers 以及更多)。

每當須要一個簡單的「保留這個類」或「保留這個方法」規則時,我實際上更喜歡在類或成員上添加 @Keep 註解的簡單性,由於它離代碼很近,幾乎就像文檔同樣。

若是其餘開發者想要在我以後重構代碼,他們會當即知道被 @Keep 標記的類 / 成員須要特殊處理,而沒必要記住和參考 ProGuard 配置而且冒着破壞某些東西的風險。IDE 中大部分的代碼重構也應當自動保留類的 @Keep 註解。

Plaid 統計信息

這有一些來自 Plaid 的統計信息,它們展現了我經過使用 ProGuard 刪除了多少代碼。在有更多依賴和更大 DEX 的更復雜的應用上,節省的可能更多。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索