原文:https://developer.android.com/studio/build/shrink-code.html
要盡可能減小 APK 文件,您應(yīng)該啟用壓縮來移除發(fā)布構(gòu)建中未使用的代碼和資源。此頁面介紹如何執(zhí)行該操作,以及如何指定要在構(gòu)建時(shí)保留或舍棄的代碼和資源。
代碼壓縮通過 ProGuard 提供,ProGuard 會(huì)檢測(cè)和移除封裝應(yīng)用中未使用的類、字段、方法和屬性,包括自帶代碼庫中的未使用項(xiàng)(這使其成為以變通方式解決64k 引用限制的有用工具)。ProGuard 還可優(yōu)化字節(jié)碼,移除未使用的代碼指令,以及用短名稱混淆其余的類、字段和方法?;煜^的代碼可令您的 APK 難以被逆向工程,這在應(yīng)用使用許可驗(yàn)證等安全敏感性功能時(shí)特別有用。
資源壓縮通過 Android Plugin for Gradle 提供,該插件會(huì)移除封裝應(yīng)用中未使用的資源,包括代碼庫中未使用的資源。它可與代碼壓縮發(fā)揮協(xié)同效應(yīng),使得在移除未使用的代碼后,任何不再被引用的資源也能安全地移除。
本文介紹的功能依賴下列組件:
SDK Tools25.0.10 或更高版本
Android Plugin for Gradle2.0.0 或更高版本
壓縮代碼
要啟用通過 ProGuard 實(shí)現(xiàn)的代碼壓縮,請(qǐng)?jiān)赽uild.gradle文件相應(yīng)的構(gòu)建類型中添加minifyEnabled true。
請(qǐng)注意,代碼壓縮會(huì)拖慢構(gòu)建速度,因此您應(yīng)該盡可能避免在調(diào)試構(gòu)建中使用。不過,重要的是您一定要為用于測(cè)試的最終 APK 啟用代碼壓縮,因?yàn)槿绻荒艹浞值?a target="_blank" rel="nofollow">自定義要保留的代碼,可能會(huì)引入錯(cuò)誤。
例如,下面這段來自build.gradle文件的代碼用于為發(fā)布構(gòu)建啟用代碼壓縮:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile(‘proguard-android.txt'),
'proguard-rules.pro'
}
}
...
}
注:Android Studio 會(huì)在使用Instant Run時(shí)停用 ProGuard。
除了minifyEnabled屬性外,還有用于定義 ProGuard 規(guī)則的proguardFiles屬性:
getDefaultProguardFile(‘proguard-android.txt')方法可從 Android SDKtools/proguard/文件夾獲取默認(rèn) ProGuard 設(shè)置。
提示:要想做進(jìn)一步的代碼壓縮,可嘗試使用位于同一位置的proguard-android-optimize.txt文件。它包括相同的 ProGuard 規(guī)則,但還包括其他在字節(jié)碼一級(jí)(方法內(nèi)和方法間)執(zhí)行分析的優(yōu)化,以進(jìn)一步減小 APK 大小和幫助提高其運(yùn)行速度。
proguard-rules.pro文件用于添加自定義 ProGuard 規(guī)則。默認(rèn)情況下,該文件位于模塊根目錄(build.gradle文件旁)。
要添加更多各構(gòu)建變體專用的 ProGuard 規(guī)則,請(qǐng)?jiān)谙鄳?yīng)的productFlavor代碼塊中再添加一個(gè)proguardFiles屬性。例如,以下 Gradle 文件會(huì)向flavor2產(chǎn)品風(fēng)味添加flavor2-rules.pro。現(xiàn)在flavor2使用所有三個(gè) 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)建時(shí) ProGuard 都會(huì)輸出下列文件:
dump.txt
說明 APK 中所有類文件的內(nèi)部結(jié)構(gòu)。
mapping.txt
提供原始與混淆過的類、方法和字段名稱之間的轉(zhuǎn)換。
seeds.txt
列出未進(jìn)行混淆的類和成員。
usage.txt
列出從 APK 移除的代碼。
這些文件保存在/build/outputs/mapping/release/。
自定義要保留的代碼
對(duì)于某些情況,默認(rèn) ProGuard 配置文件 (proguard-android.txt) 足以滿足需要,ProGuard 會(huì)移除所有(并且只會(huì)移除)未使用的代碼。不過,ProGuard 難以對(duì)許多情況進(jìn)行正確分析,可能會(huì)移除應(yīng)用真正需要的代碼。舉例來說,它可能錯(cuò)誤移除代碼的情況包括:
當(dāng)應(yīng)用引用的類只來自AndroidManifest.xml文件時(shí)
當(dāng)應(yīng)用調(diào)用的方法來自 Java 原生接口 (JNI) 時(shí)
當(dāng)應(yīng)用在運(yùn)行時(shí)(例如使用反射或自檢)操作代碼時(shí)
測(cè)試應(yīng)用應(yīng)該能夠發(fā)現(xiàn)因不當(dāng)移除而導(dǎo)致的錯(cuò)誤,但您還可通過查看/build/outputs/mapping/release/中保存的usage.txt輸出文件來檢查移除了哪些代碼。
要修正錯(cuò)誤并強(qiáng)制 ProGuard 保留特定代碼,請(qǐng)?jiān)?ProGuard 配置文件中添加一行-keep代碼。例如:
-keeppublicclassMyClass
您還可以向您想保留的代碼添加@Keep注解。在類上添加@Keep可原樣保留整個(gè)類。在方法或字段上添加它可完整保留方法/字段(及其名稱)以及類名稱。請(qǐng)注意,只有在使用注解支持庫時(shí),才能使用此注解。
在使用-keep選項(xiàng)時(shí),有許多事項(xiàng)需要考慮;如需了解有關(guān)自定義配置文件的詳細(xì)信息,請(qǐng)閱讀ProGuard 手冊(cè)。問題排查一章概述了您可能會(huì)在混淆代碼時(shí)遇到的其他常見問題。
解碼混淆過的堆疊追蹤
在 ProGuard 壓縮代碼后,讀取堆疊追蹤變得困難(即使并非不可行),因?yàn)榉椒Q經(jīng)過了混淆處理。幸運(yùn)的是,ProGuard 每次運(yùn)行都會(huì)創(chuàng)建一個(gè)mapping.txt文件,其中顯示了與混淆過的名稱對(duì)應(yīng)的原始類名稱、方法名稱和字段名稱。ProGuard 將該文件保存在應(yīng)用的/build/outputs/mapping/release/目錄中。
請(qǐng)注意,您每次使用 ProGuard 創(chuàng)建發(fā)布構(gòu)建時(shí)都會(huì)覆蓋mapping.txt文件,因此您每次發(fā)布新版本時(shí)都必須小心地保存一個(gè)副本。通過為每個(gè)發(fā)布構(gòu)建保留一個(gè)mapping.txt文件副本,您就可以在用戶提交的已混淆堆疊追蹤來自舊版本應(yīng)用時(shí)對(duì)問題進(jìn)行調(diào)試。
在 Google Play 上發(fā)布應(yīng)用時(shí),您可以上傳每個(gè) APK 版本的mapping.txt文件。Google Play 將根據(jù)用戶報(bào)告的問題對(duì)收到的堆疊追蹤進(jìn)行去混淆處理,以便您在 Google Play Developer Console 中進(jìn)行檢查。如需了解詳細(xì)信息,請(qǐng)參閱幫助中心有關(guān)如何對(duì)崩潰堆疊追蹤進(jìn)行去混淆處理的文章。
要自行將混淆過的堆疊追蹤轉(zhuǎn)換成可讀的堆疊追蹤,請(qǐng)使用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 工具會(huì)從標(biāo)準(zhǔn)輸入讀取。
壓縮資源
資源壓縮只與代碼壓縮協(xié)同工作。代碼壓縮器移除所有未使用的代碼后,資源壓縮器便可確定應(yīng)用仍然使用的資源。這在您添加包含資源的代碼庫時(shí)體現(xiàn)得尤為明顯——您必須移除未使用的內(nèi)容庫代碼,使內(nèi)容庫資源變?yōu)槲匆觅Y源,才能通過資源壓縮器將它們移除。
要啟用資源壓縮,請(qǐng)?jiān)赽uild.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)用,請(qǐng)先嘗試使用它,然后再啟用shrinkResources,因?yàn)槟赡苄枰庉媝roguard-rules.pro文件以保留動(dòng)態(tài)創(chuàng)建或調(diào)用的類或方法,然后再開始移除資源。
注:資源壓縮器目前不會(huì)移除values/文件夾中定義的資源(例如字符串、尺寸、樣式和顏色)。這是因?yàn)?Android 資源打包工具 (AAPT) 不允許 Gradle 插件為資源指定預(yù)定義版本。有關(guān)詳情,請(qǐng)參閱問題 70869。
自定義要保留的資源
如果您有明確想要保留或舍棄的資源,請(qǐng)?jiān)谀捻?xiàng)目中創(chuàng)建一個(gè)包含標(biāo)記的 XML 文件,并在tools:keep屬性中指定每個(gè)要保留的資源,在tools:discard屬性中指定每個(gè)要舍棄的資源。這兩個(gè)屬性都接受逗號(hào)分隔的資源名稱列表。您可以使用星號(hào)字符作為通配符。
例如:
tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
tools:discard="@layout/unused2"/>
將該文件保存在項(xiàng)目資源中,例如,保存在res/raw/keep.xml。構(gòu)建不會(huì)將該文件打包到 APK 之中。
指定要舍棄的資源可能看似愚蠢,因?yàn)槟究蓪⑺鼈儎h除,但在使用構(gòu)建變體時(shí),這樣做可能很有用。例如,如果您明知給定資源表面上會(huì)在代碼中使用(并因此不會(huì)被壓縮器移除),但實(shí)際不會(huì)用于給定構(gòu)建變體,就可以將所有資源放入同一項(xiàng)目目錄,然后為每個(gè)構(gòu)建變體創(chuàng)建一個(gè)不同的keep.xml文件。
啟用嚴(yán)格引用檢查
正常情況下,資源壓縮器可準(zhǔn)確判定系統(tǒng)是否使用了資源。不過,如果您的代碼調(diào)用Resources.getIdentifier()(或您的任何內(nèi)容庫進(jìn)行了這一調(diào)用——AppCompat內(nèi)容庫會(huì)執(zhí)行該調(diào)用),這就表示您的代碼是根據(jù)動(dòng)態(tài)生成的字符串查詢資源名稱。當(dāng)您執(zhí)行這一調(diào)用時(shí),默認(rèn)情況下資源壓縮器會(huì)采取防御性行為,將所有具有匹配名稱格式的資源標(biāo)記為可能已使用,無法移除。
例如,以下代碼會(huì)使系統(tǒng)將所有帶img_前綴的資源標(biāo)記為已使用。
Stringname=String.format("img_%1d",angle+1);
res=getResources().getIdentifier(name,"drawable",getPackageName());
資源壓縮器還會(huì)瀏覽代碼以及各種res/raw/資源中的所有字符串常量,尋找格式類似于file:///android_res/drawable//ic_plus_anim_016.png的資源網(wǎng)址。如果它找到與其類似的字符串,或找到其他看似可用來構(gòu)建與其類似的網(wǎng)址的字符串,則不會(huì)將它們移除。
這些是默認(rèn)情況下啟用的安全壓縮模式的示例。但您可以停用這一“有備無患”處理方式,并指定資源壓縮器只保留其確定已使用的資源。要執(zhí)行此操作,請(qǐng)?jiān)趉eep.xml中將shrinkMode設(shè)置為strict,如下所示:
tools:shrinkMode="strict"/>
如果您確已啟用嚴(yán)格壓縮模式,并且代碼也引用了包含動(dòng)態(tài)生成字符串的資源(如上所示),您就必須利用tools:keep屬性手動(dòng)保留這些資源。
移除未使用的備用資源
Gradle 資源壓縮器只會(huì)移除未被您的應(yīng)用代碼引用的資源,這意味著它不會(huì)移除用于不同設(shè)備配置的備用資源。必要時(shí),您可以使用 Android Gradle 插件的resConfigs屬性來移除您的應(yīng)用不需要的備用資源文件。
例如,如果您使用的內(nèi)容庫包含語言資源(例如使用的是 AppCompat 或 Google Play 服務(wù)),則 APK 將包括這些內(nèi)容庫中消息的所有已翻譯語言字符串,無論應(yīng)用的其余部分是否翻譯為同一語言。如果您想只保留應(yīng)用正式支持的語言,可以利用resConfig屬性指定這些語言。系統(tǒng)會(huì)移除未指定語言的所有資源。
下面這段代碼展示了如何將語言資源限定為僅支持英語和法語:
android {
defaultConfig {
...
resConfigs "en", "fr"
}
}
同理,您也可以利用APK 拆分為不同設(shè)備構(gòu)建不同的 APK,自定義在 APK 中包括的屏幕密度或 ABI 資源。
合并重復(fù)資源
默認(rèn)情況下,Gradle 還會(huì)合并同名資源,例如可能位于不同資源文件夾中的同名可繪制對(duì)象。這一行為不受shrinkResources屬性控制,也無法停用,因?yàn)樵谟卸鄠€(gè)資源匹配代碼查詢的名稱時(shí),有必要利用這一行為來避免錯(cuò)誤。
只有在兩個(gè)或更多個(gè)文件具有完全相同的資源名稱、類型和限定符時(shí),才會(huì)進(jìn)行資源合并。Gradle 會(huì)在重復(fù)項(xiàng)中選擇其視為最佳選擇的文件(根據(jù)下述優(yōu)先順序),并只將這一個(gè)資源傳遞給 AAPT,以供在 APK 文件中分發(fā)。
Gradle 會(huì)在下列位置尋找重復(fù)資源:
與主源集關(guān)聯(lián)的主資源,一般位于src/main/res/。
變體疊加,來自構(gòu)建類型和構(gòu)建風(fēng)味。
內(nèi)容庫項(xiàng)目依賴項(xiàng)。
Gradle 會(huì)按以下級(jí)聯(lián)優(yōu)先順序合并重復(fù)資源:
依賴項(xiàng) → 主資源 → 構(gòu)建風(fēng)味 → 構(gòu)建類型
例如,如果某個(gè)重復(fù)資源同時(shí)出現(xiàn)在主資源和構(gòu)建風(fēng)味中,Gradle 會(huì)選擇構(gòu)建風(fēng)味中的重復(fù)資源。
如果完全相同的資源出現(xiàn)在同一源集中,Gradle 無法合并它們,并且會(huì)發(fā)出資源合并錯(cuò)誤。如果您在build.gradle文件的sourceSet屬性中定義了多個(gè)源集,在特定情況下(例如src/main/res/和src/main/res2/包含完全相同的資源時(shí)),就可能發(fā)生這種情況。
排查資源壓縮問題
當(dāng)您壓縮資源時(shí),Gradle Console 會(huì)顯示它從應(yīng)用軟件包中移除的資源的摘要。例如:
:android:shrinkDebugResources
Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning
Gradle 還會(huì)在/build/outputs/mapping/release/(ProGuard 輸出文件所在的文件夾)中創(chuàng)建一個(gè)名為resources.txt的診斷文件。該文件包括諸如哪些資源引用了其他資源以及使用或移除了哪些資源等詳情。
例如,要了解您的 APK 為何仍包含@drawable/ic_plus_anim_016,請(qǐng)打開resources.txt文件并搜索該文件名。您可能會(huì)發(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可以訪問——如果您向上搜索,就會(huì)發(fā)現(xiàn)“The root reachable resources are:”之下列有該資源。這意味著存在對(duì)add_schedule_fab_icon_anim的代碼引用(即在可訪問代碼中找到了其 R.drawable ID)。
如果您使用的不是嚴(yán)格檢查,則存在看似可用于為動(dòng)態(tài)加載資源構(gòu)建資源名稱的字符串常量時(shí),可將資源 ID 標(biāo)記為可訪問。在這種情況下,如果您在構(gòu)建輸出中搜索資源名稱,可能會(huì)找到類似下面這樣的消息:
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.
如果您看到一個(gè)這樣的字符串,并且您能確定該字符串未用于動(dòng)態(tài)加載給定資源,就可以按照有關(guān)如何自定義要保留的資源部分中所述利用tools:discard屬性通知構(gòu)建系統(tǒng)將它移除。