要盡可能減小 APK 文件匈子,您應(yīng)該啟用壓縮來移除發(fā)布構(gòu)建中未使用的代碼和資源河胎。此頁面介紹如何執(zhí)行該操作,以及如何指定要在構(gòu)建時保留或舍棄的代碼和資源虎敦。
代碼壓縮通過 ProGuard 提供游岳,ProGuard 會檢測和移除封裝應(yīng)用中未使用的類、字段其徙、方法和屬性胚迫,包括自帶代碼庫中的未使用項(xiàng)(這使其成為以變通方式解決?64k 引用限制的有用工具)。ProGuard 還可優(yōu)化字節(jié)碼唾那,移除未使用的代碼指令访锻,以及用短名稱混淆其余的類、字段和方法通贞±嗜簦混淆過的代碼可令您的 APK 難以被逆向工程,這在應(yīng)用使用許可驗(yàn)證等安全敏感性功能時特別有用昌罩。
資源壓縮通過適用于 Gradle 的 Android 插件提供哭懈,該插件會移除封裝應(yīng)用中未使用的資源,包括代碼庫中未使用的資源茎用。它可與代碼壓縮發(fā)揮協(xié)同效應(yīng)遣总,使得在移除未使用的代碼后睬罗,任何不再被引用的資源也能安全地移除。
本文介紹的功能依賴下列組件:
SDK Tools?25.0.10 或更高版本
適用于 Gradle 的 Android 插件?2.0.0 或更高版本
壓縮代碼
要通過 ProGuard 啟用代碼壓縮旭斥,請?jiān)?build.gradle?文件內(nèi)相應(yīng)的構(gòu)建類型中添加?minifyEnabled true容达。
請注意,代碼壓縮會拖慢構(gòu)建速度垂券,因此您應(yīng)該盡可能避免在調(diào)試構(gòu)建中使用花盐。不過,重要的是您一定要為用于測試的最終 APK 啟用代碼壓縮菇爪,因?yàn)槿绻荒艹浞值?a target="_blank">自定義要保留的代碼算芯,可能會引入錯誤。
例如凳宙,下面這段來自?build.gradle?文件的代碼用于為發(fā)布構(gòu)建啟用代碼壓縮:
````
android {
? ? buildTypes {
? ? ? ? release {
? ? ? ? ? ? minifyEnabled true
? ? ? ? ? ? proguardFiles getDefaultProguardFile('proguard-android.txt'),
? ? ? ? ? ? ? ? ? ? 'proguard-rules.pro'
? ? ? ? }
? ? }
}
````
注:Android Studio 會在使用?Instant Run?時停用 ProGuard熙揍。如果您需要為增量式構(gòu)建壓縮代碼,請嘗試試用 Gradle 壓縮器氏涩。
除了?minifyEnabled?屬性外届囚,還有用于定義 ProGuard 規(guī)則的?proguardFiles?屬性:
getDefaultProguardFile('proguard-android.txt')?方法可從 Android SDK?tools/proguard/?文件夾獲取默認(rèn)的 ProGuard 設(shè)置。
提示:要想做進(jìn)一步的代碼壓縮是尖,請嘗試使用位于同一位置的?proguard-android-optimize.txt?文件意系。它包括相同的 ProGuard 規(guī)則,但還包括其他在字節(jié)碼一級(方法內(nèi)和方法間)執(zhí)行分析的優(yōu)化析砸,以進(jìn)一步減小 APK 大小和幫助提高其運(yùn)行速度昔字。
proguard-rules.pro?文件用于添加自定義 ProGuard 規(guī)則。默認(rèn)情況下首繁,該文件位于模塊根目錄(build.gradle?文件旁)。
要添加更多各構(gòu)建變體專用的 ProGuard 規(guī)則陨囊,請?jiān)谙鄳?yīng)的?productFlavor?代碼塊中再添加一個?proguardFiles?屬性弦疮。例如,以下 Gradle 文件會向?flavor2?產(chǎn)品定制添加?flavor2-rules.pro≈┐祝現(xiàn)在?flavor2?使用所有三個 ProGuard 規(guī)則胁塞,因?yàn)檫€應(yīng)用了來自?release?代碼塊的規(guī)則。
````
android {
? ? buildTypes {
? ? ? ? release {
? ? ? ? ? ? minifyEnabled true
? ? ? ? ? ? proguardFiles getDefaultProguardFile('proguard-android.txt'),
? ? ? ? ? ? ? ? ? 'proguard-rules.pro'
? ? ? ? }
? ? }
? ? productFlavors {
? ? ? ? flavor1 {
? ? ? ? }
? ? ? ? flavor2 {
? ? ? ? ? ? proguardFile 'flavor2-rules.pro'
? ? ? ? }
? ? }
}
````
每次構(gòu)建時 ProGuard 都會輸出下列文件:
dump.txt
說明 APK 中所有類文件的內(nèi)部結(jié)構(gòu)压语。
mapping.txt
提供原始與混淆過的類啸罢、方法和字段名稱之間的轉(zhuǎn)換。
seeds.txt
列出未進(jìn)行混淆的類和成員胎食。
usage.txt
列出從 APK 移除的代碼扰才。
這些文件保存在?/build/outputs/mapping/release/?中。
自定義要保留的代碼
對于某些情況厕怜,默認(rèn) ProGuard 配置文件 (proguard-android.txt) 足以滿足需要衩匣,ProGuard 會移除所有(并且只會移除)未使用的代碼蕾总。不過,ProGuard 難以對許多情況進(jìn)行正確分析琅捏,可能會移除應(yīng)用真正需要的代碼生百。舉例來說,它可能錯誤移除代碼的情況包括:
當(dāng)應(yīng)用引用的類只來自?AndroidManifest.xml?文件時
當(dāng)應(yīng)用調(diào)用的方法來自 Java 原生接口 (JNI) 時
當(dāng)應(yīng)用在運(yùn)行時(例如使用反射或自檢)操作代碼時
測試應(yīng)用應(yīng)該能夠發(fā)現(xiàn)因不當(dāng)移除的代碼而導(dǎo)致的錯誤柄延,但您也可以通過查看?/build/outputs/mapping/release/?中保存的?usage.txt?輸出文件來檢查移除了哪些代碼蚀浆。
要修正錯誤并強(qiáng)制 ProGuard 保留特定代碼,請?jiān)?ProGuard 配置文件中添加一行?-keep?代碼搜吧。例如:
-keep public class MyClass
或者蜡坊,您可以向您想保留的代碼添加?@Keep?注解。在類上添加?@Keep?可原樣保留整個類赎败。在方法或字段上添加它可完整保留方法/字段(及其名稱)以及類名稱秕衙。請注意,只有在使用注解支持庫時僵刮,才能使用此注解据忘。
在使用?-keep?選項(xiàng)時,有許多事項(xiàng)需要考慮搞糕;如需了解有關(guān)自定義配置文件的詳細(xì)信息勇吊,請閱讀?ProGuard 手冊。問題排查一章概述了您可能會在混淆代碼時遇到的其他常見問題窍仰。
解碼混淆過的堆疊追蹤
在 ProGuard 壓縮代碼后汉规,讀取堆疊追蹤變得困難(即使并非不可行),因?yàn)榉椒Q經(jīng)過了混淆處理驹吮。幸運(yùn)的是针史,ProGuard 每次運(yùn)行時都會創(chuàng)建一個?mapping.txt?文件,其中顯示了與混淆過的名稱對應(yīng)的原始類名稱碟狞、方法名稱和字段名稱啄枕。ProGuard 將該文件保存在應(yīng)用的?/build/outputs/mapping/release/?目錄中。
請注意族沃,您每次使用 ProGuard 創(chuàng)建發(fā)布構(gòu)建時都會覆蓋?mapping.txt?文件频祝,因此您每次發(fā)布新版本時都必須小心地保存一個副本。通過為每個發(fā)布構(gòu)建保留一個?mapping.txt?文件副本脆淹,您就可以在用戶提交的已混淆堆疊追蹤來自舊版本應(yīng)用時對問題進(jìn)行調(diào)試常空。
在 Google Play 上發(fā)布應(yīng)用時,您可以上傳每個 APK 版本的?mapping.txt?文件盖溺。Google Play 將根據(jù)用戶報(bào)告的問題對收到的堆疊追蹤進(jìn)行去混淆處理漓糙,以便您在 Google Play Developer Console 中進(jìn)行檢查。如需了解詳細(xì)信息咐柜,請參閱幫助中心有關(guān)如何對崩潰堆疊追蹤進(jìn)行去混淆處理的文章兼蜈。
要自行將混淆過的堆疊追蹤轉(zhuǎn)換成可讀的堆疊追蹤攘残,請使用?retrace?腳本(在 Windows 上為?retrace.bat;在 Mac/Linux 上為?retrace.sh)为狸。它位于?/tools/proguard/?目錄中歼郭。該腳本利用?mapping.txt?文件和您的堆疊追蹤生成新的可讀堆疊追蹤。使用 retrace 工具的語法如下:
retrace.bat|retrace.sh [-verbose] mapping.txt []
例如:
retrace.bat -verbose mapping.txt obfuscated_trace.txt
如果您不指定堆疊追蹤文件辐棒,retrace 工具會從標(biāo)準(zhǔn)輸入讀取病曾。
通過 Instant Run 啟用代碼壓縮
如果代碼壓縮在您增量構(gòu)建應(yīng)用時非常重要,請嘗試適用于 Gradle 的 Android 插件內(nèi)置的試用代碼壓縮器漾根。與 ProGuard 不同泰涂,此壓縮器支持?Instant Run。
您也可以使用與 ProGuard 相同的配置文件來配置 Android 插件壓縮器辐怕。但是逼蒙,Android 插件壓縮器不會對您的代碼進(jìn)行混淆處理或優(yōu)化,它只會刪除未使用的代碼寄疏。因此是牢,您應(yīng)該僅將其用于調(diào)試構(gòu)建,并為發(fā)布構(gòu)建啟用 ProGuard陕截,以便對發(fā)布 APK 的代碼進(jìn)行混淆處理和優(yōu)化驳棱。
要啟用 Android 插件壓縮器,只需在 "debug" 構(gòu)建類型中將?useProguard?設(shè)置為?false(并保留?minifyEnabled?設(shè)置?true):
````
android {? ? buildTypes {? ? ? ? debug {? ? ? ? ? ? minifyEnabled trueuseProguard falseproguardFiles getDefaultProguardFile('proguard-android.txt'),? ? ? ? ? ? ? ? ? ? 'proguard-rules.pro'? ? ? ? }? ? ? ? release {? ? ? ? ? ? minifyEnabled true? ? ? ? ? ? proguardFiles getDefaultProguardFile('proguard-android.txt'),? ? ? ? ? ? ? ? ? ? 'proguard-rules.pro'? ? ? ? }? ? }}
````
注:如果 Android 插件壓縮器最初刪除了某個方法农曲,但您之后更改了代碼社搅,使該方法可訪問,Instant Run 會將其視為結(jié)構(gòu)代碼更改并執(zhí)行冷交換乳规。
壓縮資源
資源壓縮只與代碼壓縮協(xié)同工作形葬。代碼壓縮器移除所有未使用的代碼后,資源壓縮器便可確定應(yīng)用仍然使用的資源驯妄。這在您添加包含資源的代碼庫時體現(xiàn)得尤為明顯 - 您必須移除未使用的庫代碼荷并,使庫資源變?yōu)槲匆觅Y源,才能通過資源壓縮器將它們移除青扔。
要啟用資源壓縮,請?jiān)?build.gradle?文件中將?shrinkResources?屬性設(shè)置為?true(在用于代碼壓縮的?minifyEnabled?旁邊)翩伪。例如:
````
android {
? ? buildTypes {
? ? ? ? release {
? ? ? ? ? ? shrinkResources true
? ? ? ? ? ? minifyEnabled true
? ? ? ? ? ? proguardFiles getDefaultProguardFile('proguard-android.txt'),
? ? ? ? ? ? ? ? ? ? 'proguard-rules.pro'
? ? ? ? }
? ? }
}
````
如果您尚未使用代碼壓縮用途的?minifyEnabled?構(gòu)建應(yīng)用微猖,請先嘗試使用它,然后再啟用?shrinkResources缘屹,因?yàn)槟赡苄枰庉?proguard-rules.pro?文件以保留動態(tài)創(chuàng)建或調(diào)用的類或方法凛剥,然后再開始移除資源轻姿。
注:資源壓縮器目前不會移除?values/?文件夾中定義的資源(例如字符串逻炊、尺寸犁享、樣式和顏色)。這是因?yàn)?Android 資源打包工具 (AAPT) 不允許 Gradle 插件為資源指定預(yù)定義版本炊昆。有關(guān)詳情,請參閱問題 70869凤巨。
自定義要保留的資源
如果您有想要保留或舍棄的特定資源,請?jiān)谀捻?xiàng)目中創(chuàng)建一個包含??標(biāo)記的 XML 文件敢茁,并在?tools:keep?屬性中指定每個要保留的資源,在?tools:discard?屬性中指定每個要舍棄的資源彰檬。這兩個屬性都接受逗號分隔的資源名稱列表。您可以使用星號字符作為通配符。
例如:
將該文件保存在項(xiàng)目資源中谭梗,例如激捏,保存在?res/raw/keep.xml凄吏。構(gòu)建不會將該文件打包到 APK 之中。
指定要舍棄的資源可能看似愚蠢图柏,因?yàn)槟究蓪⑺鼈儎h除,但在使用構(gòu)建變體時繁涂,這樣做可能很有用。例如步势,如果您明知給定資源表面上會在代碼中使用(并因此不會被壓縮器移除),但實(shí)際不會用于給定構(gòu)建變體坏瘩,就可以將所有資源放入公用項(xiàng)目目錄柱锹,然后為每個構(gòu)建變體創(chuàng)建一個不同的?keep.xml?文件瞧毙。構(gòu)建工具也可能無法根據(jù)需要正確識別資源宙彪,這是因?yàn)榫幾g器會添加內(nèi)聯(lián)資源 ID男图,而資源分析器可能不知道真正引用的資源和恰巧具有相同值的代碼中的整數(shù)值之間的差別逊笆。
啟用嚴(yán)格引用檢查
正常情況下譬胎,資源壓縮器可準(zhǔn)確判定系統(tǒng)是否使用了資源。不過骗污,如果您的代碼調(diào)用?Resources.getIdentifier()(或您的任何庫進(jìn)行了這一調(diào)用 -?AppCompat?庫會執(zhí)行該調(diào)用),這就表示您的代碼將根據(jù)動態(tài)生成的字符串查詢資源名稱沈条。當(dāng)您執(zhí)行這一調(diào)用時需忿,默認(rèn)情況下資源壓縮器會采取防御性行為,將所有具有匹配名稱格式的資源標(biāo)記為可能已使用蜡歹,無法移除屋厘。
例如,以下代碼會使所有帶?img_?前綴的資源標(biāo)記為已使用月而。
String name = String.format("img_%1d", angle + 1);
res
= getResources().getIdentifier(name, "drawable", getPackageName());
資源壓縮器還會瀏覽代碼以及各種?res/raw/?資源中的所有字符串常量汗洒,尋找格式類似于?file:///android_res/drawable//ic_plus_anim_016.png?的資源網(wǎng)址。如果它找到與其類似的字符串父款,或找到其他看似可用來構(gòu)建與其類似的網(wǎng)址的字符串溢谤,則不會將它們移除。
這些是默認(rèn)情況下啟用的安全壓縮模式的示例铛漓。但您可以停用這一“有備無患”處理方式溯香,并指定資源壓縮器只保留其確定已使用的資源。要執(zhí)行此操作玫坛,請?jiān)?keep.xml?文件中將?shrinkMode?設(shè)置為?strict湿镀,如下所示:
如果您確已啟用嚴(yán)格壓縮模式伐憾,并且代碼也引用了包含動態(tài)生成字符串的資源(如上所示)树肃,則必須利用?tools:keep?屬性手動保留這些資源。
移除未使用的備用資源
Gradle 資源壓縮器只會移除未被您的應(yīng)用代碼引用的資源斩祭,這意味著它不會移除用于不同設(shè)備配置的備用資源摧玫。必要時绑青,您可以使用 Android Gradle 插件的?resConfigs?屬性來移除您的應(yīng)用不需要的備用資源文件闸婴。
例如掠拳,如果您使用的庫包含語言資源(例如使用的是 AppCompat 或 Google Play 服務(wù)),則 APK 將包括這些庫中消息的所有已翻譯語言字符串喊熟,無論應(yīng)用的其余部分是否翻譯為同一語言芥牌。如果您想只保留應(yīng)用正式支持的語言壁拉,則可以利用?resConfig?屬性指定這些語言弃理。系統(tǒng)會移除未指定語言的所有資源屎蜓。
下面這段代碼展示了如何將語言資源限定為僅支持英語和法語:
````
android {
? ? defaultConfig {
? ? ? ? resConfigs "en", "fr"
? ? }
}
````
同理炬转,您也可以利用?APK 拆分為不同設(shè)備構(gòu)建不同的 APK扼劈,自定義在 APK 中包括的屏幕密度或 ABI 資源荐吵。
合并重復(fù)資源
默認(rèn)情況下赊瞬,Gradle 還會合并同名資源森逮,例如可能位于不同資源文件夾中的同名可繪制對象。這一行為不受?shrinkResources?屬性控制良风,也無法停用烟央,因?yàn)樵谟卸鄠€資源匹配代碼查詢的名稱時,有必要利用這一行為來避免錯誤粮呢。
只有在兩個或更多個文件具有完全相同的資源名稱啄寡、類型和限定符時哩照,才會進(jìn)行資源合并。Gradle 會在重復(fù)項(xiàng)中選擇其視為最佳選擇的文件(根據(jù)下述優(yōu)先順序)识藤,并只將這一個資源傳遞給 AAPT痴昧,以供在 APK 文件中分發(fā)赶撰。
Gradle 會在下列位置尋找重復(fù)資源:
與主源集關(guān)聯(lián)的主資源版确,一般位于?src/main/res/?中绒疗。
變體疊加,來自構(gòu)建類型和構(gòu)建風(fēng)味吓蘑。
庫項(xiàng)目依賴項(xiàng)。
Gradle 會按以下級聯(lián)優(yōu)先順序合并重復(fù)資源:
依賴項(xiàng) → 主資源 → 構(gòu)建風(fēng)味 → 構(gòu)建類型
例如溃蔫,如果某個重復(fù)資源同時出現(xiàn)在主資源和構(gòu)建風(fēng)味中伟叛,Gradle 會選擇構(gòu)建風(fēng)味中的重復(fù)資源统刮。
如果完全相同的資源出現(xiàn)在同一源集中,Gradle 無法合并它們暗膜,并且會發(fā)出資源合并錯誤学搜。如果您在?build.gradle?文件的?sourceSet?屬性中定義了多個源集论衍,則可能會發(fā)生這種情況饲齐,例如捂人,如果?src/main/res/?和?src/main/res2/?包含完全相同的資源滥搭,就可能會發(fā)生這種情況。
排查資源壓縮問題
當(dāng)您壓縮資源時闽坡,Gradle Console 會顯示它從應(yīng)用軟件包中移除的資源的摘要疾嗅。例如:
````
:android:shrinkDebugResources
Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning
````
Gradle 還會在?/build/outputs/mapping/release/(ProGuard 輸出文件所在的文件夾)中創(chuàng)建一個名為?resources.txt 的診斷文件冕象。該文件包括諸如哪些資源引用了其他資源以及使用或移除了哪些資源等詳情渐扮。
例如,要了解您的 APK 為何仍包含?@drawable/ic_plus_anim_016膀估,請打開?resources.txt?文件并搜索該文件名。您可能會發(fā)現(xiàn)帕棉,有其他資源引用了它笤昨,如下所示:
16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]? ? @drawable/ic_plus_anim_016
現(xiàn)在您需要了解為何?@drawable/add_schedule_fab_icon_anim?可以訪問 - 如果您向上搜索握恳,就會發(fā)現(xiàn)“The root reachable resources are:”之下列有該資源乡洼。這意味著存在對?add_schedule_fab_icon_anim?的代碼引用(即在可訪問代碼中找到了其 R.drawable ID)束昵。
如果您使用的不是嚴(yán)格檢查锹雏,則存在看似可用于為動態(tài)加載資源構(gòu)建資源名稱的字符串常量時礁遵,可將資源 ID 標(biāo)記為可訪問佣耐。在這種情況下兼砖,如果您在構(gòu)建輸出中搜索資源名稱讽挟,可能會找到類似下面這樣的消息:
10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
? ? used because it format-string matches string pool constant ic_plus_anim_%1$d.
如果您看到一個這樣的字符串丸冕,并且您能確定該字符串未用于動態(tài)加載給定資源褐墅,就可以按照有關(guān)如何自定義要保留的資源部分中所述利用?tools:discard?屬性通知構(gòu)建系統(tǒng)將它移除。