[譯] 實用 ProGuard 規則示例
來自專欄掘金翻譯計劃
- 原文地址:Practical ProGuard rules examples
- 原文作者:Wojtek Kaliciński
- 譯文出自:掘金翻譯計劃
- 本文永久鏈接:https://github.com/xitu/gold-miner/blob/master/TODO1/practical-proguard-rules-examples.md
- 譯者:Derek
- 校對者:BillShiyaoZhang
實用 ProGuard 規則示例
我在之前的文章中解釋了 為什麼每個人都應該將 ProGuard 用於他們的 Android 應用、怎麼啟用它以及在使用中可能面臨的錯誤種類。這其中涉及很多理論,因為我認為理解基本原理以準備好處理任何潛在問題非常重要。
我還在一篇單獨的文章中談到了 為 Instant App 構建配置 ProGuard 的非常具體的問題。
在這裡,我想談 ProGuard 規則在中型樣例應用上的實用示例:出自 Nick Butcher 的 Plaid.
從 Plaid 中吸取的教訓
Plaid 實際上是研究 ProGuard 問題的一個很好的主題,因為它包含使用註解處理與代碼生成、反射、Java資源載入和原生代碼(JNI)的第三方庫的混合體。我提取並記錄下了一些適用於其他應用的實用建議:
數據類
public class User { String name; int age; ...}
每個應用可能都有某種數據類(也被稱為 DMOs,模型等,取決於上下文以及它們處在應用架構中的位置)。關於數據對象的事實是,通常在某些時候他們將被載入或保存(序列化)到某些其他介質中,例如網路(HTTP 請求)、資料庫(通過 ORM)、磁碟上的 JSON 文件或 Firebase 數據存儲。
許多簡化序列化與反序列化這些欄位的工具依賴於反射。GSON、Retrofit、Firebase —— 他們都檢查數據類的欄位名並把它們轉換成另一種表現形式(例如:{「name」: 「Sue」, 「age」: 28}
),用於傳輸或存儲。它們將數據讀入 Java 對象時也是同理 —— 它們看到鍵值對 「name」:」John」
並嘗試通過查找 String name
欄位將其應用到 Java 對象上。
結論:我們不能讓 ProGuard 重命名或刪除這些數據類的任何欄位,因為它們必須與序列化的格式匹配。最好給整個類添加一個 @Keep
註解或者給所有模型添加通配符規則:
-keep class io.plaidapp.data.api.dribbble.model.** { *; }
警告:在測試你的應用是否容易受到這個問題的影響是可能會出錯。例如,如果你在版本 N 的應用程序中將一個對象序列化成 JSON 並將其保存到磁碟而沒有使用適當的 keep 規則,那麼保存的數據可能看起來像這樣:
然而,當你再一次構建你的應用並發布版本 N+1 的應用時,ProGuard 可能會決定將你的欄位重命名為某些其他的,比如{「a」: 「Sue」, 「b」: 28}
。因為 ProGuard 將你的欄位重命名為a
和b
,所以一切看起來似乎都有效,數據也會被正確地保存和載入。c
和d
。因此,之前保存的數據將無法載入。 首先你必須確保你有適當的 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 / getResource
和 ClassLoader.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 的情況)。另請注意,在某些情況下,隨庫提供的配置只能在禁用優化的條件下起作用,因此如果你開啟了優化,那麼你可能踏入了未知領域。
那麼當庫沒有提供規則時,如何制定規則呢?
我只能給你一些提示:- 閱讀構建輸出和 logcat!
- 構建警告會告訴你添加哪些
-dontwarn
規則 ClassNotFoundException
、MethodNotFoundException
和FieldNotFoundException
會告訴你添加哪些-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 默認會刪除代碼中的所有註解甚至一些剩餘的類型信息。對於一些庫來說,這不是個問題 —— 那些在編譯時處理註解與生成代碼的庫(例如 Dagger2 或 Glide 等等)可能以後程序運行時不需要這些註解。
還有另外一類實際上在運行時檢查註解或查看參數與異常的類型信息的工具。例如 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.(額外的優化),你必須使用其他默認的 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 鏈接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。
推薦閱讀:
※Android控制項RadioButton的應用:做切換界面的標籤欄。
※【技術流】Android 截屏監聽:如何實現截圖分享功能?
※Android自動化測試之Monkey工具(上)
※基於 Android NDK 的學習之旅-----Android.mk 介紹
TAG:Android |