ProGuard 在 Android 上的使用姿勢

Android_開發者發表於2018-01-15

為什麼使用 ProGuard

ProGuard 是一個壓縮、優化、混淆程式碼的工具。儘管有很多其他工具供開發者們使用,但是 ProGuard 作為 Android Gradle 構建過程的一部分,已經打包在 SDK 中。

當我們構建應用時,使用 ProGuard 有很多好處。有的開發者更關心混淆這塊功能,對我而言最大的用處是打包時移除 dex 中的無用程式碼。

ProGuard 在 Android 上的使用姿勢

一個 Android 示例應用的空間分佈圖,原始碼地址 Topeka sample app

減少包體積的好處有很多,比如增加使用者黏性和滿意度,提升下載速度,減少安裝時間,以便在終端裝置上連線使用者,尤其是在新興市場。當然,有時候您不得不限制您的應用的大小,比如 Instant App 限制大小 4 MB,此時 ProGuard 顯得必不可少了。

如果以上還不足以說服您使用 ProGuard,其實移除無用程式碼和混淆所有名稱還有其他更多的優化效果:

  • 在一些版本的 Android 裝置上,DEX 程式碼會在安裝或者執行時被編譯成機器碼。原始的 DEX 和優化後的機器碼都會保留在裝置中,所以算一下就知道:程式碼越少,意味著編譯時間越短,儲存佔用越少
  • ProGuard 除了可以大幅減少程式碼的空間之外,還可以讓所有的識別符號(包、類和成員)都使用更短的名字,如 a.Aa.a.B。這個過程就是混淆。混淆通過兩種方式來減少程式碼:讓表示名稱的字串更短;在這些方法或者屬性有相同的簽名情況,下這些字串更容易被複用,最終減少了字串池的數目。
  • 使用 ProGuard 是開啟資源壓縮的前提條件. 資源壓縮功能會移除您專案中程式碼沒有引用到的資原始檔(如圖片資源,這一般是 APK 中佔比最大的部分了).
  • 通過僅將您程式碼中實際使用的方法打包到 APK 中,移除程式碼會幫您避免 64K dex 方法引用問題。尤其是您引用了很多第三方庫的時候,這樣可以大大降低在您應用中使用 Multidex 的需求。

每個 Android 應用都應該使用程式碼壓縮嗎?我認為是的!

但是在您激動的跳起來之前,請先繼續閱讀下去。當您開啟 ProGuard 時,在某些非常微妙的情況下會讓您的應用崩潰。雖然有些錯誤會在構建應用時發生,您能及時發現,但是也有些錯誤您只能在執行時發現,所以請確保您的應用經過徹底的測試。

如何使用 ProGuard?

在您的專案中開啟 ProGuard 只需簡單到新增如下幾行程式碼在您的主應用模組的 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’
  }
}
複製程式碼

ProGuard 自身的配置已經在另外一個單獨的配置檔案中完成了。上面的程式碼中,我給出了 Android Gradle 打包外掛中的預設配置¹,接下去我會在 proguard-rules.pro 中加入其他的配置。

在 ProGuard 官網您可以找到一個 使用手冊。 在您深入研究這些配置之前,最好先大概理解 ProGuard 是如何工作的和我們為什麼要指定一些額外的選項。

ProGuard 在 Android 上的使用姿勢

您也可以去觀看 part of this Google I/O session Shai Barack 的教學視訊。

簡單來說,ProGuard 將您專案中的 .class 檔案做為輸入,然後尋找程式碼中所有的呼叫點,計算出程式碼中所有可達的呼叫關係圖,然後移除剩餘的部分(即不可達的程式碼和那些不會被呼叫的程式碼)。

在您讀 ProGuard 手冊時,您沒必要看那些 輸入 / 輸出的部分,因為這些 Android Gradle 打包外掛會替您指定輸入源(您和第三方庫的程式碼) 和 Android jar 庫(您構建應用時用到的 Android 框架類)。

想要正確配置 ProGuard,最重要的就是讓它知道執行時您的哪些程式碼不應該被移除(如果開啟混淆的話,當然也要保持他們的名稱不變)。當一些類和方法會被動態訪問到時(如使用反射),在某些情況下,ProGuard 在構建呼叫圖時不能正確的決定他們的「生死」,導致這些程式碼被錯誤的移除掉。當您只從 XML 資源引用您的程式碼會時(通常使用底層的反射),這個情況也會發生。

在一次 Android 典型的構建過程中,AAPT(處理資源的工具)會生成一個額外的 ProGuard 規則檔案。它會為 Android 應用新增一些特別的 keep 規則,所以您在 Android Manifest.xml 中記錄的 Activities、Services、BroadcastReceivers 和 ContentProviders 會保持不動. 這就是為什麼在上面動圖中 MyActivity 類沒有被被移除或者重新命名.

AAPT 也會 keep 住所有在 XML 佈局檔案使用到的 View 類(和它們的建構函式)和其他一些類,如在過渡動畫資源中引用到的過渡類。 您可以在構建後直接看這個 AAPT 生成的配置檔案,位置是:<your_project>/<app_module>/build/intermediates/proguard-rules/<variant>/aapt_rules.txt

ProGuard 在 Android 上的使用姿勢

在構建時 AAPT 生成的一個示例 ProGuard 配置檔案

我會在本文後面章節中討論更多關於 keep 規則,但是在那之前我們最好先學一下在以下情況時應該怎麼做:

當 ProGuard 打斷了您的構建

在您可以測試是否開啟 ProGuard 後所有程式碼在執行時都能正常工作前,您需要先構建您的應用。不幸的是,ProGuard 可能會發現一些引用的類缺失,並給予告警,導致您的構建失敗。

修復這個問題的關鍵是仔細觀察構建時輸出的訊息,理解這些警告的內容並定位他們。通常的途徑是修正您的依賴或者在您的 ProGuard 配置中新增 -dontwarn 規則。

這些警告的一個原因就是,您的構建路徑中沒有加入需要依賴的 JARs,如使用了 provided (僅編譯時)依賴。而有時候,在 Android 上這些程式碼的依賴在執行時並不會被真正的呼叫。讓我們看一個真實的例子。

ProGuard 在 Android 上的使用姿勢

一個專案依賴 OkHttp 3.8.0 構建時的訊息。

OkHttp 庫在 3.8.0 版本的類中新增了新的註解(javax.annotation.Nullable)。但是因為它們使用了編譯時的依賴,所以這些註解在最終構建時不會被打包進去(哪怕應用顯式的依賴了 com.google.code.findbugs:jsr305),因此 ProGuard 會抱怨 缺失了這些類.

因為我們知道這些註解類在執行時不會被使用,我們可以通過在 ProGuard 配置中新增 -dontwarn 規則來安全地忽略掉這些警告,如 在 OkHttp 文件中加入這些規則

-dontwarn javax.annotation.Nullable  
-dontwarn javax.annotation.ParametersAreNonnullByDefault
複製程式碼

您應該經歷過類似的過程,在輸出訊息中看到這些警告,然後重新構建直到構建通過。重要的是去理解為什麼您會收到這些警告以及您在構建時是否真的缺少這些類。

現在您可能會嘗試使用 -ignorewarnings 選項直接忽略所有的警告,但這通常不是個好注意。在某些情況下,ProGuard 的警告確實有助於您發現閃退的罪魁禍首和關於您配置上的其他問題

您可能需要了解一下 Progard的 notes (優先順序低於警告的訊息),它可以幫您發現一些反射相關的問題。雖然它不會打斷您的構建,但是在執行時可能會閃退。這會在下面的場景中發生:

當 ProGuard 移除過多的類

在某些情況下,ProGuard 並不知道一個類或者方法被使用了,例如這個類僅在反射時被使用或者僅在 XML 中被引用。為了阻止這樣的程式碼被移除或混淆,您應當在 ProGuard 配置中指定額外 keep 規則。這取決於作為應用開發者的你,需要去發現哪些部分程式碼有問題並提供必要的規則。

當執行時發生了 ClassNotFoundExceptionMethodNotFoundException 異常意味著您肯定缺失了某些類或者方法,也許是 ProGuard 移除了他們,又或者是因為錯誤配置依賴而導致無法找到他們。所以生產環境的構建(開啟 ProGuard 時)一定要注重徹底的測試並正視這些錯誤。

您有很多選項來配置您的 ProGuard:

  • **keep **— 保留所有匹配的類和方法
  • **keepclassmembers **— 當且僅當它們的類因為其他的原因被保留時(被其他呼叫點引用到或者被其他的規則 keep 住),keep 住指定的一些成員
  • **keepclasseswithmembers **— 當且僅當所有的成員在匹配的類中存在時,會 keep 住 這些類和它的成員

我建議您從 ProGuard 的這篇 class specification syntax 開始熟悉,此文討論了上述所有的 keep 規則和前一段討論到的 -dontwarn 選項。另外這三個 keep 規則也各有一個不同的版本支援僅保留混淆(重新命名),不保留壓縮。您可以在 ProGuard 官網的表格看一下概覽。

作為一個可選的方案來寫 ProGuard 規則,您可以直接在某個不想被混淆和移除的類、方法、屬性上新增 @Keep 註解。注意,如果這樣做的話,您需要把 Android 預設的 ProGuard 配置加入到您的構建中。

APK Analyzer 和 ProGuard

Android Studio 整合的 APK Analyzer 可以幫您看到哪些類被 ProGuard 移除了並支援為它們生成 keep 規則。當您構建 APK 時開啟了 ProGuard,那麼會額外輸出一些檔案在 <app_module>/build/outputs/mapping/ 目錄下。這些檔案包含了移除程式碼的資訊、混淆的對映關係。

ProGuard 在 Android 上的使用姿勢

載入 ProGuard 對映檔案到 APK Analyzer 可以看到 DEX 檢視中更多的資訊

當您載入了對映檔案到 APK Analyzer時(點選 “Load Proguard mappings… “ 按鈕), 您可以在 DEX 檢視樹中看到一些額外功能:

  • 所有的名字都是混淆前的(即您可以看到原始的名字)
  • 被 ProGuard 配置規則 kept 的包,類,方法和屬性會顯示成粗體
  • 您可以開啟 “Show removed nodes” 選項來看任何被 ProGuard 移除的內容(字型上會有刪除線)。右擊樹上的一個節點可以讓您生成一個 keep 規則以便您貼上到您的配置檔案中。

當 ProGuard 移除過少的類

所有應用都可以使用 Android 內建的 ProGuard 的一些安全的預設規則,如保留 View 的 getter 和 setter 方法,因為他們通常會被反射來訪問,以及其他一些普通的方法和類都不會被移除。 這在許多情況下可以時您的應用避免崩潰的發生,但是這些配置並不是 100% 適合您的應用。您可以移除掉預設的 ProGuard 檔案而使用您自己的。

如果您希望 ProGuard 移除所有未使用的程式碼,您應當避免 keep 規則寫的太寬泛,如加入萬用字元匹配整個包,而是使用類相關的匹配規則或者使用上面提及的 @Keep 註解。

ProGuard 在 Android 上的使用姿勢

使用 -whyareyoukeeping <class-specification> 選項來觀察為什麼這些類沒有被移除。

如果您實在不確定為什麼 ProGuard 沒有移除您期望它移除的程式碼,,您可以新增 -whyareyoukeeping 選項至 ProGuard 配置檔案中,然後重新構建您的應用。在構建輸出中,您會看到是什麼呼叫鏈決定了 ProGuard 保留這些程式碼。

ProGuard 在 Android 上的使用姿勢

在 APK Analyzer 中追蹤是什麼在 DEX 中 keep 住了這些類和方法

另一種方法不那麼精準,但在任何應用都不需要重新構建和額外的工作量。那就是在 APK Analyzer 中開啟 DEX 檔案,然後右擊您關注的類、方法。選擇 “Find usages” 您將看到引用鏈,這也許會引導您瞭解哪部分程式碼使用指定的類、方法從而阻止了它被移除。

ProGuard 和 混淆後的堆疊

我之前提及到,在構建過程中 ProGuard 會在處理類檔案時輸出對映關係和日誌檔案。當您需要保留構建產物時,您應當儲存好這些檔案和 APK 在一起。這些對映檔案不能被其他的構建所使用,而只會在與它們一起生成的 APK 配合使用時才能確保正確。有了這些對映關係,您才能有效地 debug 使用者裝置的發生的崩潰。否則太難去定位問題了,因為名字都混淆過了。

ProGuard 在 Android 上的使用姿勢

上傳 APK 對應的 ProGuard 對映檔案至 Google Play 控制檯,從而獲得混淆前的堆疊資訊。

您在 Google Play 控制檯釋出混淆後的生產 APK時,記得為每個版本上傳對應的對映檔案。這樣的話當您看 ANRs & crashes 頁面時,上報的堆疊都會現實真實的類名、方法名和行號而不是縮短的混淆後的那些。

關於 ProGuard 和 第三方庫

就像您有責任為您自己的程式碼提供 keep 規則一樣,那些第三方庫的作者們也有義務向您提供必要的混淆規則配置來避免開啟 Proguard 導致的構建失敗或者應用崩潰。

有些專案簡單地在他們的文件或者 README 上提及了必要的混淆規則,所以您需要複製貼上這些規則到您的主 ProGuard 配置檔案中。不過有個更好的方法,第三方庫的維護者們如果釋出的庫是 AAR ,那麼可以指定規則打包在 AAR 中並會在應用構建時自動暴露給構建系統,通過新增下面幾行程式碼到庫模組的 build.gradle 檔案中:

release { //or your own build type  
  consumerProguardFiles ‘consumer-proguard.txt’  
}
複製程式碼

您寫入在 consumer-proguard.txt 檔案中的規則將會在應用構建時附加到應用主 ProGuard 配置並被使用。


如果想了解更多關於程式碼和資源壓縮的資訊,請參考我們的文件頁面


開啟 ProGuard 可能一開始會比較困難,但是我個人認為這些代價是值得的。只要投入一點點時間,您將會獲得一個輕量、優化後的應用。此外,現在花費時間去配置您的應用意味著當實驗性的 ProGuard 替代者 R8 就緒時,您已經準備好了。因為 R8 也是用現有的 ProGuard 規則檔案來工作的。

除了讓您的程式碼更小巧之外, ProGuard 和 R8 可以選擇優化您的程式碼讓它執行得更快,當然這又是另一篇文章的話題了……


¹ proguard-android.txt 檔案之前是在 SDK tools 目錄下(SDK/tools/proguard/proguard-android.txt),但在新版的 SDK Tools 和 Android Gradle 外掛版本2.2.0+上,可以在構建時從 Android 外掛的 jar 中解壓出來。在構建您的專案後,您可以在 <your_project>/build/intermediates/proguard-files/ 目錄下找到這個配置檔案。

感謝 Daniel Galpin


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章