本文介紹了Android中開啟混淆的好處,混淆的工作原理及如何解決開啟混淆后遇到的問題。
原文鏈接:Troubleshooting ProGuard issues on Android
《行路難》
金樽清酒斗十千,玉盤珍饈直萬錢。
停杯投箸不能食,拔劍四顧心茫然。
欲渡黃河冰塞川,將登太行雪滿山。
閑來垂釣坐溪上,忽復(fù)乘舟夢日邊。
行路難,行路難,多歧路,今安在。
長風(fēng)破浪會有時,直掛云帆濟滄海。
—唐,李白
為什么混淆
混淆器(ProGuard)是一個壓縮、優(yōu)化和混淆代碼的工具。當(dāng)然開發(fā)者也可以使用其它工具,混淆器作為 Android Gradle 構(gòu)建處理的一部分并且附帶在SDK中可以很方便使用。
你開發(fā)的應(yīng)用想要開啟混淆的原因可能有多種。有些開發(fā)者可能關(guān)心混淆了多少代碼,但對我來說最大的好處是可以刪除所有未使用的代碼,否則會作為 classes.dex 文件的一部分打包到 APK 中。

圖 Android 應(yīng)用大小分布餅圖的示例。數(shù)據(jù)來源:Topeka sample app。
讓你的代碼大小更小可以帶來很多實際好處,例如,提高用戶留存率和滿意度,更快的下載和安裝時間,安裝在用戶的低端設(shè)備上,尤其是新興市場。還有一些情況,當(dāng)你需要限制應(yīng)用的大小,例如 4MB limit for Instant Apps,這種情況下混淆肯定是必不可少的。
如果這對你還不夠方便,考慮移除未使用的代碼并且混淆所有的名稱會有不錯的效果,還可以開啟更多優(yōu)化:
- 在一些 Android 版本上,DEX 代碼會在安裝時或運行時編譯成機器碼。原始的 DEX 和優(yōu)化后的代碼會一直保留在設(shè)備上,因此這是個很簡單的數(shù)學(xué)問題,更少的代碼代表在設(shè)備上更短的編譯時間和更少的存儲使用。
- 混淆可以做的另一個事情是,在代碼大小上有很大的影響,它會修改所有的標(biāo)識符(包名,類名和成員變量)為短名稱,例如 a.A 和 a.a.B。這個處理是眾所周知的混淆?;煜ㄟ^兩種方式減少代碼大?。捍韺嶋H字符串的這些名稱更短,此外如果它們共享了相同的簽名,它們有更高的可能性被不同的方法和域重用,這會減少字符串池中 item 的總數(shù)量。
- 使用混淆器需要開啟資源壓縮。資源壓縮會移除在你的工程中沒有使用代碼引用的資源(例如圖片,通常是APK中占比最大的一部分)。
- 移除代碼也可以幫你避免 dex 64k 方法數(shù)限制問題。通過只打包代碼中實際使用的方法到APK中,尤其是當(dāng)你考慮做第三方類庫時,你可以在應(yīng)用中減少使用 Multidex 的需要。
你覺得每個應(yīng)用都應(yīng)該開啟代碼壓縮么?是的!
開始使用之前,先學(xué)習(xí)下開啟混淆后可能遇到的一些問題。構(gòu)建應(yīng)用時可能會出現(xiàn)一些錯誤,還有只能在運行時才能捕獲到的錯誤,因此需要徹底測試你的應(yīng)用。
怎樣混淆
在應(yīng)用 module 的 build.gradle 文件中添加如下代碼:
buildTypes {
/* you will normally want to enable ProGuard only for your release builds, as it’s an additional step that makes the build slower and can make debugging more difficult */
release {
minifyEnabled true
proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
}
}
通過分別指定配置文件完成混淆配置。通過上面的代碼可以看到我添加了 android gradle 插件提供的默認(rèn)配置,并且在 proguard-rules.pro 文件中添加了一些工程相關(guān)的配置。在官網(wǎng)上你可以找到可手動配置的所有選項。在你深入研究配置選項之前,最好先理解混淆是怎樣工作的以及我們?yōu)槭裁葱枰付~外的選項。

圖 你也可以觀看 Google I/O 大會上 Shai Barack’s 解釋。
簡而言之,混淆器會將工程中的類文件作為輸入,搜索應(yīng)用入口點的所有可能性并且從這些入口點計算出所有代碼可達性的地圖,然后移除剩下的代碼(無用代碼,或永遠不會運行的代碼,因為它從未被調(diào)用)。
閱讀混淆手冊的時候,應(yīng)當(dāng)跳過輸入/輸出部分,Android gradle 插件會為你指定輸入(你的類文件)和類庫 jar 包。
正確的配置混淆的部分是讓它知道哪一部分代碼是在運行時訪問并且不應(yīng)該被移除(當(dāng)混淆打開后它們的名字會保持原樣)。當(dāng)類或方法是通過動態(tài)訪問(使用反射),混淆器在構(gòu)建被使用的代碼的地圖時有時并不知道這些代碼是否被使用并且會錯誤地將這些類移除。這也會發(fā)生在只從xml資源中引用代碼時(通常在底層使用反射的方式)。
在 android 構(gòu)建期間,AAPT(處理資源的工具)會生成一個額外的混淆規(guī)則文件。它為 android 應(yīng)用的入口點添加顯式 keep 規(guī)則,因此清單文件中所有的 Activities,Services,BroadcastReceivers 和 ContentProviders 會保持原樣。這就是上面的動畫中 MyActivity 類沒有被移除或重命名的原因。
AAPT 也會 keep 所有在 xml 布局中使用的 Views(以及它們的構(gòu)造方法)和一些其它類,例如在動畫過渡資源中引用的過渡類。你可以在執(zhí)行構(gòu)建之后檢查 AAPT 生成的配置文件,通過打開 <your_project>/<app_module>/build/intermediates/proguard-rules/<variant>/aapt_rules.txt 文件:

圖 構(gòu)建期間 AAPT 創(chuàng)建的混淆配置示例
在后面的部分我們會講到 keep 規(guī)則,在此之前我們最好先學(xué)習(xí)下它做了些什么。
當(dāng)開啟混淆后導(dǎo)致構(gòu)建失敗
測試應(yīng)用開啟混淆后是否可以正常工作之前,應(yīng)該先構(gòu)建應(yīng)用。當(dāng)混淆檢查出你的代碼有問題,它會在編譯時發(fā)出警告并導(dǎo)致構(gòu)建失敗,例如引用不存在的類。
解決構(gòu)建失敗的關(guān)鍵在于查看構(gòu)建輸出的日志,理解警告是關(guān)于什么的及它們的地址,通常通過修復(fù)依賴或在混淆配置中添加 -dontwarn 規(guī)則解決。
警告出現(xiàn)的其中一個原因是利用 JARs 包編譯的依賴但沒有添加到編譯路徑,例如,當(dāng)使用 provided(只在編譯時使用)依賴。有時候,使用這些依賴的代碼路徑在 Android 上運行類庫代碼時不是實際被調(diào)用的。我們來看一個真實的例子。
關(guān)于構(gòu)建依賴的詳細(xì)說明請查看 Gradle 構(gòu)建依賴配置說明。

圖 構(gòu)建依賴 OkHttp 3.8.0 工程的警告輸出
OkHttp 類庫的3.8.0版本在類上添加了新的注解 (javax.annotation.Nullable),因為它們使用了編譯時依賴,注解本身不會打包到依賴 OkHttp 的應(yīng)用(除非應(yīng)用顯式地添加了com.google.code.findbugs:jsr305)并且混淆器會輸出找不到的類信息。
因為我們知道這些注解類在運行時不會被使用,我們可以在混淆配置中添加 -dontwarn 規(guī)則安全地忽略警告,正如 OkHttp 所建議的那樣:
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
你應(yīng)該對所有的警告做同樣的處理,然后重新構(gòu)建直到構(gòu)建成功。重要的是應(yīng)該理解為什么會出現(xiàn)這些警告,忽略它可能是安全的,也有可能在構(gòu)建時真的丟失了一些類。
現(xiàn)在你可能會嘗試使用 ignorewarnings 選項忽略所有的警告,但這并不是一個好主意。在某些情況下,混淆警告會讓你了解讓應(yīng)用無法正常工作的錯誤,和你配置的其它問題。
你也有可能會想要查看混淆日志,可以突出顯示通過反射訪問的類的問題。如果沒有導(dǎo)致構(gòu)建失敗,這些會導(dǎo)致令人討厭的運行時漰潰。
當(dāng)混淆移除了有用的代碼
在某些情況下,混淆不知道一個類或方法是否被使用,例如它只被反射或從 XML 中引用。為了不讓類被混淆或被移除,需要在混淆配置中指定額外 keep 規(guī)則。這需要你處理有問題的代碼并添加必要的規(guī)則。
在運行時得到 ClassNotFoundException 或 MethodNotFoundException 錯誤表示丟失了類或方法,可能由于混淆移除了類或由于錯誤的依賴配置導(dǎo)致。測試應(yīng)用的 release 構(gòu)建(開啟混淆)并處理這些錯誤是很重要的。
這有幾個不同的 keep 選項,你可以用于配置混淆:
- keep——保留所有匹配類規(guī)范的類和方法
- keepclassmembers——指定被保留的成員,但前提是它們的父類由于某些原因(從入口點是可達的或被別的規(guī)則保留)被保留
- keepclasseswithmembers——會保留類及它的成員,但前提是在類規(guī)范列出的所有成員
我建議你好好看看類規(guī)范語法,用于上面提到的所有 keep 規(guī)則以及前面部分提到的 -dontwarn 選項。這三條 keep 規(guī)則只會阻止混淆(重命名),不會阻止壓縮。你可以在混淆網(wǎng)站上找到在一個表格中所有 keep 選項的概覽。
另一個代替編寫復(fù)雜混淆規(guī)則的方法,只需要在不想要被混淆器移除或重命名的類/方法/域上添加 @Keep 注解。使用這個方法需要添加默認(rèn)的Android混淆配置文件。
APK分析器和混淆
Android Studio 中的 APK 分析器可以幫助你看到被混淆器移除的類以及為它們生成 keep 規(guī)則。當(dāng)你開啟混淆構(gòu)建 APK,會生成一個額外的輸出文件 <app_module>/build/outputs/mapping/,包含移除代碼的信息及混淆后的名稱和原始名稱之間的映射。

圖 在 DEX 查看器中解鎖更多信息通過在 APK 分析器中加載混淆映射文件
注:此功能在Android Studio 3.0版本可用。
當(dāng)你加載映射文件到 APK 分析器中(使用 “Load Proguard mappings… ” 按鈕),會在 DEX 樹視圖中得到一些額外功能:
- 所有的名稱被反混淆(你可以看到原始名稱)
- 被混淆配置規(guī)則保留的包、類、方法和域被加粗顯示
- 你可以使用 “Show removed nodes” 選項看到被混淆移除的類(加刪除線顯示)。在樹的節(jié)點上右擊可以生成 keep 規(guī)則,你可以粘貼到混淆配置文件中。
當(dāng)混淆移除的太少
Android 混淆規(guī)則對每個 Android 應(yīng)用包含了一些安全的默認(rèn)值,例如確保 View 的 getters 和 setters——可以通過反射正常訪問,以及更多常見方法和類不會被移除。這會保證你的應(yīng)用在很多情況下不會漰潰,這個配置對你的應(yīng)用來說可能不是最理想的。你可以移除默認(rèn)的混淆文件使用你自己的。
如果你想使用混淆移除所有未使用的代碼,你應(yīng)該避免 keep 規(guī)則太廣泛,例如使用通配符匹配整個包。應(yīng)該選擇類規(guī)范規(guī)則或者使用之前提到的 @Keep 注解。

圖 使用 -whyareyoukeeping 選項查看為什么類沒有被移除
如果你不確定混淆為什么沒有移除你期望移除的代碼部分,你可以在混淆配置文件中添加 -whyareyoukeeping 選項,然后再次構(gòu)建 APK。在構(gòu)建輸出中,你可以看到讓混淆器決定保留代碼的引用鏈。

圖 在 APK 分析器中查看類和方法的引用追蹤代碼被 keep 的原因
還有一種不精確的方法,但不需要重新構(gòu)建可以應(yīng)用在任何 APK 上,在 APK 分析器中打開 DEX 文件,在你感興趣的類或方法上右擊。選擇 “Find usages” 查看引用鏈,可以看到哪一部分代碼使用了給定的類或方法,因此它沒有被移除。
混淆器和混淆堆棧跟蹤
之前提到混淆器會在構(gòu)建期間處理類文件時輸出 mappings 和 logs。當(dāng)你存儲構(gòu)建結(jié)果時應(yīng)該和 APK 一起保存這些文件。映射文件不能用于不同構(gòu)建之間并且和產(chǎn)生的 APK 一起才能正常工作。mappings 文件可以幫助你調(diào)試用戶設(shè)備上的漰潰,否則由于被混淆的名稱很難解決漰潰。

圖 上傳混淆 mapping 文件和 APK 到 Google Play 控制臺得到反混淆堆棧跟蹤
當(dāng)你在 Play 控制臺發(fā)布混淆后的 APK 記得為每個版本上傳 mapping 文件。這樣的話當(dāng)你查看 ANRs & crashes 頁面,報告的堆棧跟蹤會顯示真實的類和方法名和行號,而不是被混淆后的名稱。
混淆和第三方類庫
為你自己的代碼提供 keep 規(guī)則是你的職責(zé)所在,第三方類庫的創(chuàng)建者的職責(zé)是為你提供必須的配置,因此當(dāng)你開啟混淆后構(gòu)建不會失敗或應(yīng)用不會漰潰。
一些工程在手冊或 README 文件中簡單地提到必須的規(guī)則,因此你可以復(fù)制和粘貼到你的混淆文件中。但這有一個更好的方法。對于類庫 modules 和類庫發(fā)布的 AARs,類庫的維護者可以為 AAR 提供指定的規(guī)則并自動暴露給類庫使用者的構(gòu)建系統(tǒng),通過在 build.gradle 文件中添加下面的代碼:
release { //or your own build type
consumerProguardFiles ‘consumer-proguard.txt’
}
在 consumer-proguard.txt 文件中添加的規(guī)則會被追加到主混淆配置并且在應(yīng)用構(gòu)建時被使用。
關(guān)于代碼和資源壓縮的詳細(xì)信息請參考我們的文檔。
起初開啟混淆可能會讓人覺得有點可怕,但我個人認(rèn)為它的好處是有價值的,并且只需要花一點點時間,就可以得到更小更優(yōu)化的應(yīng)用。更重要的是,現(xiàn)在花時間配置你的應(yīng)用意味著已經(jīng)做好了引入叫做 R8 的混淆替換實驗的準(zhǔn)備,它將會和現(xiàn)有混淆規(guī)則文件一起工作。
除了讓你的代碼更少,混淆和 R8 可以優(yōu)化代碼讓它運行的更快,但這是另一篇文章的主題。
注:ProGuard-android.txt 文件之前可以從 Sdk 文件夾中找到(Sdk/tools/ProGuard/ProGuard-android.txt),但在SDK的新版本和 Android Gradle plugin 2.2.0+,它會在構(gòu)建期間從 Android 插件 jar 包中解壓。你可以在構(gòu)建之后在 <your_project>/build/intermediates/ProGuard-files/ 找到配置文件。