[譯] ProGuard 在 Android 上的使用姿勢(shì)

ProGuard 在 Android 上的使用姿勢(shì)

為什么使用 ProGuard

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

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

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

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

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

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

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

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

如何使用 ProGuard?

在您的項(xiàng)目中開(kāi)啟 ProGuard 只需簡(jiǎn)單到添加如下幾行代碼在您的主應(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)在另外一個(gè)單獨(dú)的配置文件中完成了。上面的代碼中,我給出了 Android Gradle 打包插件中的默認(rèn)配置1,接下去我會(huì)在 proguard-rules.pro 中加入其他的配置。

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

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

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

在您讀 ProGuard 手冊(cè)時(shí),您沒(méi)必要看那些 輸入 / 輸出的部分,因?yàn)檫@些 Android Gradle 打包插件會(huì)替您指定輸入源(您和第三方庫(kù)的代碼) 和 Android jar 庫(kù)(您構(gòu)建應(yīng)用時(shí)用到的 Android 框架類(lèi))。

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

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

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

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

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

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

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

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

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

一個(gè)項(xiàng)目依賴 OkHttp 3.8.0 構(gòu)建時(shí)的消息。

OkHttp 庫(kù)在 3.8.0 版本的類(lèi)中添加了新的注解(javax.annotation.Nullable)。但是因?yàn)樗鼈兪褂昧司幾g時(shí)的依賴,所以這些注解在最終構(gòu)建時(shí)不會(huì)被打包進(jìn)去(哪怕應(yīng)用顯式的依賴了 com.google.code.findbugs:jsr305),因此 ProGuard 會(huì)抱怨 缺失了這些類(lèi).

因?yàn)槲覀冎肋@些注解類(lèi)在運(yùn)行時(shí)不會(huì)被使用,我們可以通過(guò)在 ProGuard 配置中添加 -dontwarn 規(guī)則來(lái)安全地忽略掉這些警告,如 在 OkHttp 文檔中加入這些規(guī)則

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

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

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

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

當(dāng) ProGuard 移除過(guò)多的類(lèi)

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

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

您有很多選項(xiàng)來(lái)配置您的 ProGuard:

  • keep?—?保留所有匹配的類(lèi)和方法
  • keepclassmembers?— 當(dāng)且僅當(dāng)它們的類(lèi)因?yàn)槠渌脑虮槐A魰r(shí)(被其他調(diào)用點(diǎn)引用到或者被其他的規(guī)則 keep ?。琸eep 住指定的一些成員
  • keepclasseswithmembers?—?當(dāng)且僅當(dāng)所有的成員在匹配的類(lèi)中存在時(shí),會(huì) keep 住 這些類(lèi)和它的成員

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

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

APK Analyzer 和 ProGuard

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

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

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

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

當(dāng) ProGuard 移除過(guò)少的類(lèi)

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

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

使用 -whyareyoukeeping <class-specification> 選項(xiàng)來(lái)觀察為什么這些類(lèi)沒(méi)有被移除。

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

在 APK Analyzer 中追蹤是什么在 DEX 中 keep 住了這些類(lèi)和方法

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

ProGuard 和 混淆后的堆棧

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

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

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

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

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

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

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

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


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


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

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


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

感謝 Daniel Galpin


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

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

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

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