(譯)解決Android混淆常見問題

本文介紹了Android中開啟混淆的好處,混淆的工作原理及如何解決開啟混淆后遇到的問題。

原文鏈接:Troubleshooting ProGuard issues on Android

《行路難》
金樽清酒斗十千,玉盤珍饈直萬錢。
停杯投箸不能食,拔劍四顧心茫然。
欲渡黃河冰塞川,將登太行雪滿山。
閑來垂釣坐溪上,忽復乘舟夢日邊。
行路難,行路難,多歧路,今安在。
長風破浪會有時,直掛云帆濟滄海。
—唐,李白

為什么混淆

混淆器(ProGuard)是一個壓縮、優化和混淆代碼的工具。當然開發者也可以使用其它工具,混淆器作為 Android Gradle 構建處理的一部分并且附帶在SDK中可以很方便使用。

你開發的應用想要開啟混淆的原因可能有多種。有些開發者可能關心混淆了多少代碼,但對我來說最大的好處是可以刪除所有未使用的代碼,否則會作為 classes.dex 文件的一部分打包到 APK 中。

圖 Android 應用大小分布餅圖的示例。數據來源:Topeka sample app

讓你的代碼大小更小可以帶來很多實際好處,例如,提高用戶留存率和滿意度,更快的下載和安裝時間,安裝在用戶的低端設備上,尤其是新興市場。還有一些情況,當你需要限制應用的大小,例如 4MB limit for Instant Apps,這種情況下混淆肯定是必不可少的。

如果這對你還不夠方便,考慮移除未使用的代碼并且混淆所有的名稱會有不錯的效果,還可以開啟更多優化:

  • 在一些 Android 版本上,DEX 代碼會在安裝時或運行時編譯成機器碼。原始的 DEX 和優化后的代碼會一直保留在設備上,因此這是個很簡單的數學問題,更少的代碼代表在設備上更短的編譯時間和更少的存儲使用
  • 混淆可以做的另一個事情是,在代碼大小上有很大的影響,它會修改所有的標識符(包名,類名和成員變量)為短名稱,例如 a.A 和 a.a.B。這個處理是眾所周知的混淆。混淆通過兩種方式減少代碼大小:代表實際字符串的這些名稱更短,此外如果它們共享了相同的簽名,它們有更高的可能性被不同的方法和域重用,這會減少字符串池中 item 的總數量。
  • 使用混淆器需要開啟資源壓縮。資源壓縮會移除在你的工程中沒有使用代碼引用的資源(例如圖片,通常是APK中占比最大的一部分)。
  • 移除代碼也可以幫你避免 dex 64k 方法數限制問題。通過只打包代碼中實際使用的方法到APK中,尤其是當你考慮做第三方類庫時,你可以在應用中減少使用 Multidex 的需要。

你覺得每個應用都應該開啟代碼壓縮么?是的!

開始使用之前,先學習下開啟混淆后可能遇到的一些問題。構建應用時可能會出現一些錯誤,還有只能在運行時才能捕獲到的錯誤,因此需要徹底測試你的應用。

怎樣混淆

在應用 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 插件提供的默認配置,并且在 proguard-rules.pro 文件中添加了一些工程相關的配置。在官網上你可以找到可手動配置的所有選項。在你深入研究配置選項之前,最好先理解混淆是怎樣工作的以及我們為什么需要指定額外的選項。

圖 你也可以觀看 Google I/O 大會上 Shai Barack’s 解釋。

簡而言之,混淆器會將工程中的類文件作為輸入,搜索應用入口點的所有可能性并且從這些入口點計算出所有代碼可達性的地圖,然后移除剩下的代碼(無用代碼,或永遠不會運行的代碼,因為它從未被調用)。

閱讀混淆手冊的時候,應當跳過輸入/輸出部分,Android gradle 插件會為你指定輸入(你的類文件)和類庫 jar 包。

正確的配置混淆的部分是讓它知道哪一部分代碼是在運行時訪問并且不應該被移除(當混淆打開后它們的名字會保持原樣)。當類或方法是通過動態訪問(使用反射),混淆器在構建被使用的代碼的地圖時有時并不知道這些代碼是否被使用并且會錯誤地將這些類移除。這也會發生在只從xml資源中引用代碼時(通常在底層使用反射的方式)。

在 android 構建期間,AAPT(處理資源的工具)會生成一個額外的混淆規則文件。它為 android 應用的入口點添加顯式 keep 規則,因此清單文件中所有的 Activities,Services,BroadcastReceivers 和 ContentProviders 會保持原樣。這就是上面的動畫中 MyActivity 類沒有被移除或重命名的原因。

AAPT 也會 keep 所有在 xml 布局中使用的 Views(以及它們的構造方法)和一些其它類,例如在動畫過渡資源中引用的過渡類。你可以在執行構建之后檢查 AAPT 生成的配置文件,通過打開 <your_project>/<app_module>/build/intermediates/proguard-rules/<variant>/aapt_rules.txt 文件:

圖 構建期間 AAPT 創建的混淆配置示例

在后面的部分我們會講到 keep 規則,在此之前我們最好先學習下它做了些什么。

當開啟混淆后導致構建失敗

測試應用開啟混淆后是否可以正常工作之前,應該先構建應用。當混淆檢查出你的代碼有問題,它會在編譯時發出警告并導致構建失敗,例如引用不存在的類。

解決構建失敗的關鍵在于查看構建輸出的日志,理解警告是關于什么的及它們的地址,通常通過修復依賴或在混淆配置中添加 -dontwarn 規則解決。

警告出現的其中一個原因是利用 JARs 包編譯的依賴但沒有添加到編譯路徑,例如,當使用 provided(只在編譯時使用)依賴。有時候,使用這些依賴的代碼路徑在 Android 上運行類庫代碼時不是實際被調用的。我們來看一個真實的例子。

關于構建依賴的詳細說明請查看 Gradle 構建依賴配置說明

圖 構建依賴 OkHttp 3.8.0 工程的警告輸出

OkHttp 類庫的3.8.0版本在類上添加了新的注解 (javax.annotation.Nullable),因為它們使用了編譯時依賴,注解本身不會打包到依賴 OkHttp 的應用(除非應用顯式地添加了com.google.code.findbugs:jsr305)并且混淆器會輸出找不到的類信息。

因為我們知道這些注解類在運行時不會被使用,我們可以在混淆配置中添加 -dontwarn 規則安全地忽略警告,正如 OkHttp 所建議的那樣:

-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault

你應該對所有的警告做同樣的處理,然后重新構建直到構建成功。重要的是應該理解為什么會出現這些警告,忽略它可能是安全的,也有可能在構建時真的丟失了一些類。

現在你可能會嘗試使用 ignorewarnings 選項忽略所有的警告,但這并不是一個好主意。在某些情況下,混淆警告會讓你了解讓應用無法正常工作的錯誤,和你配置的其它問題

你也有可能會想要查看混淆日志,可以突出顯示通過反射訪問的類的問題。如果沒有導致構建失敗,這些會導致令人討厭的運行時漰潰。

當混淆移除了有用的代碼

在某些情況下,混淆不知道一個類或方法是否被使用,例如它只被反射或從 XML 中引用。為了不讓類被混淆或被移除,需要在混淆配置中指定額外 keep 規則。這需要你處理有問題的代碼并添加必要的規則。

在運行時得到 ClassNotFoundExceptionMethodNotFoundException 錯誤表示丟失了類或方法,可能由于混淆移除了類或由于錯誤的依賴配置導致。測試應用的 release 構建(開啟混淆)并處理這些錯誤是很重要的。

這有幾個不同的 keep 選項,你可以用于配置混淆:

  • keep——保留所有匹配類規范的類和方法
  • keepclassmembers——指定被保留的成員,但前提是它們的父類由于某些原因(從入口點是可達的或被別的規則保留)被保留
  • keepclasseswithmembers——會保留類及它的成員,但前提是在類規范列出的所有成員

我建議你好好看看類規范語法,用于上面提到的所有 keep 規則以及前面部分提到的 -dontwarn 選項。這三條 keep 規則只會阻止混淆(重命名),不會阻止壓縮。你可以在混淆網站上找到在一個表格中所有 keep 選項的概覽。

另一個代替編寫復雜混淆規則的方法,只需要在不想要被混淆器移除或重命名的類/方法/域上添加 @Keep 注解。使用這個方法需要添加默認的Android混淆配置文件

APK分析器和混淆

Android Studio 中的 APK 分析器可以幫助你看到被混淆器移除的類以及為它們生成 keep 規則。當你開啟混淆構建 APK,會生成一個額外的輸出文件 <app_module>/build/outputs/mapping/,包含移除代碼的信息及混淆后的名稱和原始名稱之間的映射。

圖 在 DEX 查看器中解鎖更多信息通過在 APK 分析器中加載混淆映射文件

注:此功能在Android Studio 3.0版本可用。

當你加載映射文件到 APK 分析器中(使用 “Load Proguard mappings… ” 按鈕),會在 DEX 樹視圖中得到一些額外功能:

  • 所有的名稱被反混淆(你可以看到原始名稱)
  • 被混淆配置規則保留的包、類、方法和域被加粗顯示
  • 你可以使用 “Show removed nodes” 選項看到被混淆移除的類(加刪除線顯示)。在樹的節點上右擊可以生成 keep 規則,你可以粘貼到混淆配置文件中。

當混淆移除的太少

Android 混淆規則對每個 Android 應用包含了一些安全的默認值,例如確保 View 的 getters 和 setters——可以通過反射正常訪問,以及更多常見方法和類不會被移除。這會保證你的應用在很多情況下不會漰潰,這個配置對你的應用來說可能不是最理想的。你可以移除默認的混淆文件使用你自己的。

如果你想使用混淆移除所有未使用的代碼,你應該避免 keep 規則太廣泛,例如使用通配符匹配整個包。應該選擇類規范規則或者使用之前提到的 @Keep 注解。

圖 使用 -whyareyoukeeping 選項查看為什么類沒有被移除

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

圖 在 APK 分析器中查看類和方法的引用追蹤代碼被 keep 的原因

還有一種不精確的方法,但不需要重新構建可以應用在任何 APK 上,在 APK 分析器中打開 DEX 文件,在你感興趣的類或方法上右擊。選擇 “Find usages” 查看引用鏈,可以看到哪一部分代碼使用了給定的類或方法,因此它沒有被移除。

混淆器和混淆堆棧跟蹤

之前提到混淆器會在構建期間處理類文件時輸出 mappings 和 logs。當你存儲構建結果時應該和 APK 一起保存這些文件。映射文件不能用于不同構建之間并且和產生的 APK 一起才能正常工作。mappings 文件可以幫助你調試用戶設備上的漰潰,否則由于被混淆的名稱很難解決漰潰。

圖 上傳混淆 mapping 文件和 APK 到 Google Play 控制臺得到反混淆堆棧跟蹤

當你在 Play 控制臺發布混淆后的 APK 記得為每個版本上傳 mapping 文件。這樣的話當你查看 ANRs & crashes 頁面,報告的堆棧跟蹤會顯示真實的類和方法名和行號,而不是被混淆后的名稱。

混淆和第三方類庫

為你自己的代碼提供 keep 規則是你的職責所在,第三方類庫的創建者的職責是為你提供必須的配置,因此當你開啟混淆后構建不會失敗或應用不會漰潰。

一些工程在手冊或 README 文件中簡單地提到必須的規則,因此你可以復制和粘貼到你的混淆文件中。但這有一個更好的方法。對于類庫 modules 和類庫發布的 AARs,類庫的維護者可以為 AAR 提供指定的規則并自動暴露給類庫使用者的構建系統,通過在 build.gradle 文件中添加下面的代碼:

release { //or your own build type
  consumerProguardFiles ‘consumer-proguard.txt’
}

consumer-proguard.txt 文件中添加的規則會被追加到主混淆配置并且在應用構建時被使用。

關于代碼和資源壓縮的詳細信息請參考我們的文檔

起初開啟混淆可能會讓人覺得有點可怕,但我個人認為它的好處是有價值的,并且只需要花一點點時間,就可以得到更小更優化的應用。更重要的是,現在花時間配置你的應用意味著已經做好了引入叫做 R8 的混淆替換實驗的準備,它將會和現有混淆規則文件一起工作。

除了讓你的代碼更少,混淆和 R8 可以優化代碼讓它運行的更快,但這是另一篇文章的主題。

注:ProGuard-android.txt 文件之前可以從 Sdk 文件夾中找到(Sdk/tools/ProGuard/ProGuard-android.txt),但在SDK的新版本和 Android Gradle plugin 2.2.0+,它會在構建期間從 Android 插件 jar 包中解壓。你可以在構建之后在 <your_project>/build/intermediates/ProGuard-files/ 找到配置文件。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容