[譯] ProGuard 在 Android 上的使用姿勢

ProGuard 在 Android 上的使用姿勢

為什么使用 ProGuard

ProGuard 是一個壓縮、優(yōu)化、混淆代碼的工具。盡管有很多其他工具供開發(fā)者們使用,但是 ProGuard 作為 Android Gradle 構(gòu)建過程的一部分,已經(jīng)打包在 SDK 中。

當(dāng)我們構(gòu)建應(yīng)用時,使用 ProGuard 有很多好處。有的開發(fā)者更關(guān)心混淆這塊功能,對我而言最大的用處是打包時移除 dex 中的無用代碼。

一個 Android 示例應(yīng)用的空間分布圖,源碼地址 Topeka sample app

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

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

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

每個 Android 應(yīng)用都應(yīng)該使用代碼壓縮嗎?我認為是的!

但是在您激動的跳起來之前,請先繼續(xù)閱讀下去。當(dāng)您開啟 ProGuard 時,在某些非常微妙的情況下會讓您的應(yīng)用崩潰。雖然有些錯誤會在構(gòu)建應(yīng)用時發(fā)生,您能及時發(fā)現(xiàn),但是也有些錯誤您只能在運行時發(fā)現(xiàn),所以請確保您的應(yīng)用經(jīng)過徹底的測試。

如何使用 ProGuard?

在您的項目中開啟 ProGuard 只需簡單到添加如下幾行代碼在您的主應(yīng)用模塊的 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 自身的配置已經(jīng)在另外一個單獨的配置文件中完成了。上面的代碼中,我給出了 Android Gradle 打包插件中的默認配置1,接下去我會在 proguard-rules.pro 中加入其他的配置。

在 ProGuard 官網(wǎng)您可以找到一個 使用手冊。
在您深入研究這些配置之前,最好先大概理解 ProGuard 是如何工作的和我們?yōu)槭裁匆付ㄒ恍╊~外的選項。

您也可以去觀看 part of this Google I/O session Shai Barack 的教學(xué)視頻。

簡單來說,ProGuard 將您項目中的 .class 文件做為輸入,然后尋找代碼中所有的調(diào)用點,計算出代碼中所有可達的調(diào)用關(guān)系圖,然后移除剩余的部分(即不可達的代碼和那些不會被調(diào)用的代碼)。

在您讀 ProGuard 手冊時,您沒必要看那些 輸入 / 輸出的部分,因為這些 Android Gradle 打包插件會替您指定輸入源(您和第三方庫的代碼) 和 Android jar 庫(您構(gòu)建應(yīng)用時用到的 Android 框架類)。

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

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

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

在構(gòu)建時 AAPT 生成的一個示例 ProGuard 配置文件

我會在本文后面章節(jié)中討論更多關(guān)于 keep 規(guī)則,但是在那之前我們最好先學(xué)一下在以下情況時應(yīng)該怎么做:

當(dāng) ProGuard 打斷了您的構(gòu)建

在您可以測試是否開啟 ProGuard 后所有代碼在運行時都能正常工作前,您需要先構(gòu)建您的應(yīng)用。不幸的是,ProGuard 可能會發(fā)現(xiàn)一些引用的類缺失,并給予告警,導(dǎo)致您的構(gòu)建失敗。

修復(fù)這個問題的關(guān)鍵是仔細觀察構(gòu)建時輸出的消息,理解這些警告的內(nèi)容并定位他們。通常的途徑是修正您的依賴或者在您的 ProGuard 配置中添加 -dontwarn 規(guī)則。

這些警告的一個原因就是,您的構(gòu)建路徑中沒有加入需要依賴的 JARs,如使用了 provided (僅編譯時)依賴。而有時候,在 Android 上這些代碼的依賴在運行時并不會被真正的調(diào)用。讓我們看一個真實的例子。

一個項目依賴 OkHttp 3.8.0 構(gòu)建時的消息。

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

因為我們知道這些注解類在運行時不會被使用,我們可以通過在 ProGuard 配置中添加 -dontwarn 規(guī)則來安全地忽略掉這些警告,如 在 OkHttp 文檔中加入這些規(guī)則

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

您應(yīng)該經(jīng)歷過類似的過程,在輸出消息中看到這些警告,然后重新構(gòu)建直到構(gòu)建通過。重要的是去理解為什么您會收到這些警告以及您在構(gòu)建時是否真的缺少這些類。

現(xiàn)在您可能會嘗試使用 -ignorewarnings 選項直接忽略所有的警告,但這通常不是個好注意。在某些情況下,ProGuard 的警告確實有助于您發(fā)現(xiàn)閃退的罪魁禍?zhǔn)缀完P(guān)于您配置上的其他問題

您可能需要了解一下 Progard的 notes (優(yōu)先級低于警告的消息),它可以幫您發(fā)現(xiàn)一些反射相關(guān)的問題。雖然它不會打斷您的構(gòu)建,但是在運行時可能會閃退。這會在下面的場景中發(fā)生:

當(dāng) ProGuard 移除過多的類

在某些情況下,ProGuard 并不知道一個類或者方法被使用了,例如這個類僅在反射時被使用或者僅在 XML 中被引用。為了阻止這樣的代碼被移除或混淆,您應(yīng)當(dāng)在 ProGuard 配置中指定額外 keep 規(guī)則。這取決于作為應(yīng)用開發(fā)者的你,需要去發(fā)現(xiàn)哪些部分代碼有問題并提供必要的規(guī)則。

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

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

  • keep?—?保留所有匹配的類和方法
  • keepclassmembers?— 當(dāng)且僅當(dāng)它們的類因為其他的原因被保留時(被其他調(diào)用點引用到或者被其他的規(guī)則 keep ?。?,keep 住指定的一些成員
  • keepclasseswithmembers?—?當(dāng)且僅當(dāng)所有的成員在匹配的類中存在時,會 keep 住 這些類和它的成員

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

作為一個可選的方案來寫 ProGuard 規(guī)則,您可以直接在某個不想被混淆和移除的類、方法、屬性上添加 @Keep 注解。注意,如果這樣做的話,您需要把 Android 默認的 ProGuard 配置加入到您的構(gòu)建中。

APK Analyzer 和 ProGuard

Android Studio 集成的 APK Analyzer 可以幫您看到哪些類被 ProGuard 移除了并支持為它們生成 keep 規(guī)則。當(dāng)您構(gòu)建 APK 時開啟了 ProGuard,那么會額外輸出一些文件在 <app_module>/build/outputs/mapping/ 目錄下。這些文件包含了移除代碼的信息、混淆的映射關(guān)系。

加載 ProGuard 映射文件到 APK Analyzer 可以看到 DEX 視圖中更多的信息

當(dāng)您加載了映射文件到 APK Analyzer時(點擊 “Load Proguard mappings… “ 按鈕), 您可以在 DEX 視圖樹中看到一些額外功能:

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

當(dāng) ProGuard 移除過少的類

所有應(yīng)用都可以使用 Android 內(nèi)置的 ProGuard 的一些安全的默認規(guī)則,如保留 View 的 getter 和 setter 方法,因為他們通常會被反射來訪問,以及其他一些普通的方法和類都不會被移除。 這在許多情況下可以時您的應(yīng)用避免崩潰的發(fā)生,但是這些配置并不是 100% 適合您的應(yīng)用。您可以移除掉默認的 ProGuard 文件而使用您自己的。

如果您希望 ProGuard 移除所有未使用的代碼,您應(yīng)當(dāng)避免 keep 規(guī)則寫的太寬泛,如加入通配符匹配整個包,而是使用類相關(guān)的匹配規(guī)則或者使用上面提及的 @Keep 注解。

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

如果您實在不確定為什么 ProGuard 沒有移除您期望它移除的代碼,,您可以添加 -whyareyoukeeping 選項至 ProGuard 配置文件中,然后重新構(gòu)建您的應(yīng)用。在構(gòu)建輸出中,您會看到是什么調(diào)用鏈決定了 ProGuard 保留這些代碼。

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

另一種方法不那么精準(zhǔn),但在任何應(yīng)用都不需要重新構(gòu)建和額外的工作量。那就是在 APK Analyzer 中打開 DEX 文件,然后右擊您關(guān)注的類、方法。選擇 “Find usages” 您將看到引用鏈,這也許會引導(dǎo)您了解哪部分代碼使用指定的類、方法從而阻止了它被移除。

ProGuard 和 混淆后的堆棧

我之前提及到,在構(gòu)建過程中 ProGuard 會在處理類文件時輸出映射關(guān)系和日志文件。當(dāng)您需要保留構(gòu)建產(chǎn)物時,您應(yīng)當(dāng)保存好這些文件和 APK 在一起。這些映射文件不能被其他的構(gòu)建所使用,而只會在與它們一起生成的 APK 配合使用時才能確保正確。有了這些映射關(guān)系,您才能有效地 debug 用戶設(shè)備的發(fā)生的崩潰。否則太難去定位問題了,因為名字都混淆過了。

上傳 APK 對應(yīng)的 ProGuard 映射文件至 Google Play 控制臺,從而獲得混淆前的堆棧信息。

您在 Google Play 控制臺發(fā)布混淆后的生產(chǎn) APK時,記得為每個版本上傳對應(yīng)的映射文件。這樣的話當(dāng)您看 ANRs & crashes 頁面時,上報的堆棧都會現(xiàn)實真實的類名、方法名和行號而不是縮短的混淆后的那些。

關(guān)于 ProGuard 和 第三方庫

就像您有責(zé)任為您自己的代碼提供 keep 規(guī)則一樣,那些第三方庫的作者們也有義務(wù)向您提供必要的混淆規(guī)則配置來避免開啟 Proguard 導(dǎo)致的構(gòu)建失敗或者應(yīng)用崩潰。

有些項目簡單地在他們的文檔或者 README 上提及了必要的混淆規(guī)則,所以您需要復(fù)制粘貼這些規(guī)則到您的主 ProGuard 配置文件中。不過有個更好的方法,第三方庫的維護者們?nèi)绻l(fā)布的庫是 AAR ,那么可以指定規(guī)則打包在 AAR 中并會在應(yīng)用構(gòu)建時自動暴露給構(gòu)建系統(tǒng),通過添加下面幾行代碼到庫模塊的 build.gradle 文件中:

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

您寫入在 consumer-proguard.txt 文件中的規(guī)則將會在應(yīng)用構(gòu)建時附加到應(yīng)用主 ProGuard 配置并被使用。


如果想了解更多關(guān)于代碼和資源壓縮的信息,請參考我們的文檔頁面


開啟 ProGuard 可能一開始會比較困難,但是我個人認為這些代價是值得的。只要投入一點點時間,您將會獲得一個輕量、優(yōu)化后的應(yīng)用。此外,現(xiàn)在花費時間去配置您的應(yīng)用意味著當(dāng)實驗性的 ProGuard 替代者 R8 就緒時,您已經(jīng)準(zhǔn)備好了。因為 R8 也是用現(xiàn)有的 ProGuard 規(guī)則文件來工作的。

除了讓您的代碼更小巧之外, ProGuard 和 R8 可以選擇優(yōu)化您的代碼讓它運行得更快,當(dāng)然這又是另一篇文章的話題了……


1 proguard-android.txt 文件之前是在 SDK tools 目錄下(SDK/tools/proguard/proguard-android.txt),但在新版的 SDK Tools 和 Android Gradle 插件版本2.2.0+上,可以在構(gòu)建時從 Android 插件的 jar 中解壓出來。在構(gòu)建您的項目后,您可以在 <your_project>/build/intermediates/proguard-files/ 目錄下找到這個配置文件。

感謝 Daniel Galpin。


掘金翻譯計劃 是一個翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術(shù)文章的社區(qū),文章來源為 掘金 上的英文分享文章。內(nèi)容覆蓋 AndroidiOS、React前端、后端、產(chǎn)品、設(shè)計 等領(lǐng)域,想要查看更多優(yōu)質(zhì)譯文請持續(xù)關(guān)注 掘金翻譯計劃、官方微博知乎專欄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容