Android App包瘦身优化实践
隨著業(yè)務(wù)的快速迭代增長,美團(tuán)App里不斷引入新的業(yè)務(wù)邏輯代碼、圖片資源和第三方SDK,直接導(dǎo)致APK體積不斷增長。包體積增長帶來的問題越來越多,如CDN流量費(fèi)用增加、用戶安裝成功率降低,甚至可能會影響用戶的留存率。APK的瘦身已經(jīng)是不得不考慮的事情。在嘗試瘦身的過程中,我們借鑒了很多業(yè)界其他公司提供的方案,同時(shí)也針對自身特點(diǎn),發(fā)現(xiàn)了一些新的技巧。本文將對其中的一些做詳細(xì)介紹。
在開始講瘦身技巧之前,先來講一下APK的構(gòu)成。
APK的構(gòu)成
可以用Zip工具打開APK查看。比如,美團(tuán)App 7.8.6的線上版本的格式是這樣的:
可以看到APK由以下主要部分組成:
| lib/ | 存放so文件,可能會有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多數(shù)情況下只需要支持armabi與x86的架構(gòu)即可,如果非必需,可以考慮拿掉x86的部分 |
| res/ | 存放編譯后的資源文件,例如:drawable、layout等等 |
| assets/ | 應(yīng)用程序的資源,應(yīng)用程序可以使用AssetManager來檢索該資源 |
| META-INF/ | 該文件夾一般存放于已經(jīng)簽名的APK中,它包含了APK中所有文件的簽名摘要等信息 |
| classes(n).dex | classes文件是Java Class,被DEX編譯后可供Dalvik/ART虛擬機(jī)所理解的文件格式 |
| resources.arsc | 編譯后的二進(jìn)制資源文件 |
| AndroidManifest.xml | Android的清單文件,格式為AXML,用于描述應(yīng)用程序的名稱、版本、所需權(quán)限、注冊的四大組件 |
當(dāng)然還會有一些其它的文件,例如上圖中的org/、src/、push_version等文件或文件夾。這些資源是Java Resources,感興趣的可以結(jié)合編譯工作流中的流程圖以及MergeJavaResourcesTransform的源碼看看被打入APK包中的資源都有哪些,這里不做過多介紹。
在充分了解了APK各個(gè)組成部分以及它們的作用后,我們針對自身特點(diǎn)進(jìn)行了分析和優(yōu)化。下面將從Zip文件格式、classes.dex、資源文件、resources.arsc等方面來介紹下我們發(fā)現(xiàn)的部分優(yōu)化技巧。
Zip格式優(yōu)化
前面介紹了APK的文件格式以及主要組成部分,通過aapt l -v xxx.apk或unzip -l xxx.apk來查看APK文件時(shí)會得到以下信息,見下面截圖:
通過上圖可以看到APK中很多資源是以Stored來存儲的,根據(jù)Zip的文件格式中對壓縮方式的描述Compression_methods可以看出這些文件是沒有壓縮的,那為什么它們沒有被壓縮呢?從AAPT的源碼中找到以下描述:
/* these formats are already compressed, or don't compress well */ static const char* kNoCompressExt[] = {".jpg", ".jpeg", ".png", ".gif",".wav", ".mp2", ".mp3", ".ogg", ".aac",".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",".rtttl", ".imy", ".xmf", ".mp4", ".m4a",".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv" };可以看出AAPT在資源處理時(shí)對這些文件后綴類型的資源是不做壓縮的,那是不是可以修改它們的壓縮方式從而達(dá)到瘦身的效果呢?
在介紹怎么做之前,先來大概介紹一下App的資源是怎么被打進(jìn)APK包里的。Android構(gòu)建工具鏈?zhǔn)褂肁APT工具來對資源進(jìn)行處理,來看下圖(圖片來源于Build Workflow):
通過上圖可以看到Manifest、Resources、Assets的資源經(jīng)過AAPT處理后生成R.java、Proguard Configuration、Compiled Resources。其中R.java大家都比較熟悉,這里就不過多介紹了。我們來重點(diǎn)看看Proguard Configuration、Compiled Resources都是做什么的呢?
- Proguard Configuration是AAPT工具為Manifest中聲明的四大組件以及布局文件中(XML layouts)使用的各種Views所生成的ProGuard配置,該文件通常存放在${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/proguard-rules/${flavorName}/${buildType}/aapt_rules.txt,下面是項(xiàng)目中該文件的截圖,紅框標(biāo)記出來的就是對AndroidManifest.xml、XML Layouts中相關(guān)Class的ProGuard配置。
- Compiled Resources是一個(gè)Zip格式的文件,這個(gè)文件的路徑通常為${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/resources-${flavorName}-${buildType}-stripped.ap_。 通過下面經(jīng)過Zip解壓后的截圖,可以看出這個(gè)文件包含了res、AndroidManifest.xml和resources.arsc的文件或文件夾。結(jié)合Build Workflow中的描述,可以看出這個(gè)文件(resources-${flavorName}-${buildType}-stripped.ap_)會被apkbuilder打包到APK包中,它其實(shí)就是APK的“資源包”(res、AndroidManifest.xml和resources.arsc)。
我們就是通過這個(gè)文件來修改不同后綴文件資源的壓縮方式來達(dá)到瘦身效果的,而在后面“resources.arsc的優(yōu)化”一節(jié)中也是操作的這個(gè)文件。
筆者在自己的項(xiàng)目中是通過在package${flavorName} Task(感興趣的同學(xué)可以查看源碼)之前進(jìn)行這個(gè)操作的。
下面是部分代碼片段:
appPlugin.variantManager.variantDataList.each { variantData ->variantData.outputs.each {def sourceApFile = it.packageAndroidArtifactTask.getResourceFile();def destApFile = new File("${sourceApFile.name}.temp", sourceApFile.parentFile);it.packageAndroidArtifactTask.doFirst {byte[] buf = new byte[1024 * 8];ZipInputStream zin = new ZipInputStream(new FileInputStream(sourceApFile));ZipOutputStream out = new ZipOutputStream(new FileOutputStream(destApFile));ZipEntry entry = zin.getNextEntry();while (entry != null) {String name = entry.getName();// Add ZIP entry to output stream.ZipEntry zipEntry = new ZipEntry(name);if (ZipEntry.STORED == entry.getMethod() && !okayToCompress(entry.getName())) {zipEntry.setMethod(ZipEntry.STORED)zipEntry.setSize(entry.getSize())zipEntry.setCompressedSize(entry.getCompressedSize())zipEntry.setCrc(entry.getCrc())} else {zipEntry.setMethod(ZipEntry.DEFLATED)...}...out.putNextEntry(zipEntry);out.closeEntry();entry = zin.getNextEntry();}// Close the streamszin.close();out.close();sourceApFile.delete();destApFile.renameTo(sourceApFile);}} }當(dāng)然也可以在其它構(gòu)建步驟中采用更高壓縮率的方式來達(dá)到瘦身效果,例如采用7Zip壓縮等等。
本技巧的使用需要注意以下問題: * 如果音視頻資源被壓縮存放在APK中的話,在使用一些音頻、視頻API時(shí)尤其要注意,需要做好充分的測試。 * resources.arsc文件最好不要壓縮存儲,如果壓縮會影響一定的性能(尤其是冷啟動時(shí)間)。 * 如果想在Android 6.0上開啟android:extractNativeLibs=”false”的話,.so 文件也不能被壓縮,android:extractNativeLibs的使用姿勢看這里:App Manifest — application。
classes.dex的優(yōu)化
如何優(yōu)化classes.dex的大小呢?大體有如下套路:
- 時(shí)刻保持良好的編程習(xí)慣和對包體積敏銳的嗅覺,去除重復(fù)或者不用的代碼,慎用第三方庫,選用體積小的第三方SDK等等。
- 開啟ProGuard來進(jìn)行代碼壓縮,通過使用ProGuard來對代碼進(jìn)行混淆、優(yōu)化、壓縮等工作。
針對第一種套路,因各個(gè)公司的項(xiàng)目的差異,共性的東西較少,需要case by case的分析,這里不做過多的介紹。
壓縮代碼
可以通過開啟ProGuard來實(shí)現(xiàn)代碼壓縮,可以在build.gradle文件相應(yīng)的構(gòu)建類型中添加minifyEnabled true。
請注意,代碼壓縮會拖慢構(gòu)建速度,因此應(yīng)該盡可能避免在調(diào)試構(gòu)建中使用。不過一定要為用于測試的最終APK啟用代碼壓縮,如果不能充分地自定義要保留的代碼,可能會引入錯(cuò)誤。
例如,下面這段來自build.gradle文件的代碼用于為發(fā)布構(gòu)建啟用代碼壓縮:
android {buildTypes {release {minifyEnabled trueproguardFiles getDefaultProguardFile(‘proguard-android.txt'),'proguard-rules.pro'}}... }除了minifyEnabled屬性外,還有用于定義ProGuard規(guī)則的proguardFiles屬性:
- getDefaultProguardFile(‘proguard-android.txt')是從Android SDKtools/proguard/文件夾獲取默認(rèn)ProGuard設(shè)置。
- proguard-rules.pro文件用于添加自定義ProGuard規(guī)則。默認(rèn)情況下,該文件位于模塊根目錄(build.gradle文件旁)。
提示:要想做進(jìn)一步的代碼壓縮,可嘗試使用位于同一位置的proguard-android-optimize.txt文件。它包括相同的ProGuard規(guī)則,但還包括其他在字節(jié)碼一級(方法內(nèi)和方法間)執(zhí)行分析的優(yōu)化,以進(jìn)一步減小APK大小和幫助提高其運(yùn)行速度。
在Gradle Plugin 2.2.0及以上版本ProGuard的配置文件會自動解壓縮到${rootProject.buildDir}/${AndroidProject.FD_INTERMEDIATES}/proguard-files/目錄下,proguardFiles會從這個(gè)目錄來獲取ProGuard配置。
每次執(zhí)行完P(guān)roGuard之后,ProGuard都會在${project.buildDir}/outputs/mapping/${flavorDir}/生成以下文件:
| dump.txt | APK中所有類文件的內(nèi)部結(jié)構(gòu) |
| mapping.txt | 提供原始與混淆過的類、方法和字段名稱之間的轉(zhuǎn)換,可以通過proguard.obfuscate.MappingReader來解析 |
| seeds.txt | 列出未進(jìn)行混淆的類和成員 |
| usage.txt | 列出從APK移除的代碼 |
可以通過在usage.txt文件中看到哪些代碼被刪除了,如下圖中所示android.support.multidex.MultiDex已經(jīng)被刪除了:
R Field的優(yōu)化
除了對項(xiàng)目代碼優(yōu)化和開啟代碼壓縮之外,筆者在《美團(tuán)Android DEX自動拆包及動態(tài)加載簡介》這篇文章中提到了通過內(nèi)聯(lián)R Field來解決R Field過多導(dǎo)致MultiDex 65536的問題,而這一步驟對代碼瘦身能夠起到明顯的效果。下面是筆者通過字節(jié)碼工具在構(gòu)建流程中內(nèi)聯(lián)R Field的代碼片段(字節(jié)碼的修改可以使用Javassist或者ASM,該步驟筆者采用的是Javassist)。
ctBehaviors.each { CtBehavior ctBehavior ->if (!ctBehavior.isEmpty()) {try {ctBehavior.instrument(new ExprEditor() {@Overridepublic void edit(FieldAccess f) {try {def fieldClassName = JavassistUtils.getClassNameFromCtClass(f.getCtClass())if (shouldInlineRField(className, fieldClassName) && f.isReader()) {def temp = fieldClassName.substring(fieldClassName.indexOf(ANDROID_RESOURCE_R_FLAG) + ANDROID_RESOURCE_R_FLAG.length())def fieldName = f.fieldNamedef key = "${temp}.${fieldName}"if (resourceSymbols.containsKey(key)) {Object obj = resourceSymbols.get(key)try {if (obj instanceof Integer) {int value = ((Integer) obj).intValue()f.replace("\$_=${value};")} else if (obj instanceof Integer[]) {def obj2 = ((Integer[]) obj)StringBuilder stringBuilder = new StringBuilder()for (int index = 0; index < obj2.length; ++index) {stringBuilder.append(obj2[index].intValue())if (index != obj2.length - 1) {stringBuilder.append(",")}}f.replace("\$_ = new int[]{${stringBuilder.toString()}};")} else {throw new GradleException("Unknown ResourceSymbols Type!")}} catch (NotFoundException e) {throw new GradleException(e.message)} catch (CannotCompileException e) {throw new GradleException(e.message)}} else {throw new GradleException("******** InlineRFieldTask unprocessed ${className}, ${fieldClassName}, ${f.fieldName}, ${key}")}}} catch (NotFoundException e) {}}})} catch (CannotCompileException e) {}} }其它優(yōu)化手段
針對代碼的瘦身還有很多優(yōu)化的技巧,例如:
- 減少ENUM的使用(詳情可以參考:Remove Enumerations),每減少一個(gè)ENUM可以減少大約1.0到1.4 KB的大小;
- 通過pmd cpd來檢查重復(fù)的代碼從而進(jìn)行代碼優(yōu)化;
- 移除掉所有無用或者功能重復(fù)的依賴庫。
這些優(yōu)化技巧就不展開介紹了。
資源的優(yōu)化
圖片優(yōu)化
為了支持Android設(shè)備DPI的多樣化([l|m|tv|h|x|xx|xxx]dpi)以及用戶對高質(zhì)量UI的期待,美團(tuán)App中使用了大量的圖片,在Android下支持很多格式的圖片,例如:PNG、JPG 、WebP,那我們該怎么選擇不同類型的圖片格式呢? 在Google I/O 2016中提到了針對圖片格式的選擇,來看下圖(圖片來源于Image compression for Android developers):
通過上圖可以看出一個(gè)大概圖片格式選擇的方法。如果能用VectorDrawable來表示的話優(yōu)先使用VectorDrawable,如果支持WebP則優(yōu)先用WebP,而PNG主要用在展示透明或者簡單的圖片,而其它場景可以使用JPG格式。針對每種圖片格式也有各類的優(yōu)化手段和優(yōu)化工具。
使用矢量圖片
可以使用矢量圖形來創(chuàng)建獨(dú)立于分辨率的圖標(biāo)和其他可伸縮圖片。使用矢量圖片能夠有效的減少App中圖片所占用的大小,矢量圖形在Android中表示為VectorDrawable對象。 使用VectorDrawable對象,100字節(jié)的文件可以生成屏幕大小的清晰圖像,但系統(tǒng)渲染每個(gè)VectorDrawable對象需要大量的時(shí)間,較大的圖像需要更長的時(shí)間才能出現(xiàn)在屏幕上。 因此只有在顯示小圖像時(shí)才考慮使用矢量圖形。有關(guān)使用VectorDrawable的更多信息,請參閱 Working with Drawables。
使用WebP
如果App的minSdkVersion高于14(Android 4.0+)的話,可以選用WebP格式,因?yàn)閃ebP在同畫質(zhì)下體積更小(WebP支持透明度,壓縮比比JPEG更高但顯示效果卻不輸于JPEG,官方評測quality參數(shù)等于75均衡最佳), 可以通過PNG到WebP轉(zhuǎn)換工具來進(jìn)行轉(zhuǎn)換。當(dāng)然Android從4.0才開始WebP的原生支持,但是不支持包含透明度,直到Android 4.2.1+才支持顯示含透明度的WebP,在筆者使用中是判斷當(dāng)前App的minSdkVersion以及圖片文件的類型(是否為透明)來選用是否適用WebP。見下面的代碼片段:
boolean isPNGWebpConvertSupported() {if (!isWebpConvertEnable()) {return false}// Android 4.0+return GradleUtils.getAndroidExtension(project).defaultConfig.minSdkVersion.apiLevel >= 14// 4.0 }boolean isTransparencyPNGWebpConvertSupported() {if (!isWebpConvertEnable()) {return false}// Lossless, Transparency, Android 4.2.1+return GradleUtils.getAndroidExtension(project).defaultConfig.minSdkVersion.apiLevel >= 18// 4.3 }def convert() {String resPath = "${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/merged/${variant.dirName}"def resDir = new File("${resPath}")resDir.eachDirMatch(~/drawable[a-z0-9-]*/) { dir ->FileTree tree = project.fileTree(dir: dir)tree.filter { File file ->return (isJPGWebpConvertSupported() && (file.name.endsWith(SdkConstants.DOT_JPG) || file.name.endsWith(SdkConstants.DOT_JPEG))) || (isPNGWebpConvertSupported() && file.name.endsWith(SdkConstants.DOT_PNG) && !file.name.endsWith(SdkConstants.DOT_9PNG))}.each { File file ->def shouldConvert = trueif (file.name.endsWith(SdkConstants.DOT_PNG)) {if (!isTransparencyPNGWebpConvertSupported()) {shouldConvert = !Imaging.getImageInfo(file).isTransparent()}}if (shouldConvert) {WebpUtils.encode(project, webpFactorQuality, file.absolutePath, webp)}}} }選擇更優(yōu)的壓縮工具
可以使用pngcrush、pngquant或zopflipng等壓縮工具來減少PNG文件大小,而不會丟失圖像質(zhì)量。所有這些工具都可以減少PNG文件大小,同時(shí)保持圖像質(zhì)量。
pngcrush工具特別有效:此工具在PNG過濾器和zlib(Deflate)參數(shù)上迭代,使用過濾器和參數(shù)的每個(gè)組合來壓縮圖像。然后選擇產(chǎn)生最小壓縮輸出的配置。
對于JPEG文件,你可以使用packJPG或guetzli等工具將JPEG文件壓縮的更小,這些工具能夠在保持圖片質(zhì)量不變的情況下,把圖片文件壓縮的更小。guetzli工具更是能夠在圖片質(zhì)量不變的情況下,將文件大小降低35%。
在Android構(gòu)建流程中AAPT會使用內(nèi)置的壓縮算法來優(yōu)化res/drawable/目錄下的PNG圖片,但也可能會導(dǎo)致本來已經(jīng)優(yōu)化過的圖片體積變大,可以通過在build.gradle中設(shè)置cruncherEnabled來禁止AAPT來優(yōu)化PNG圖片。
aaptOptions {cruncherEnabled = false }開啟資源壓縮
Android的編譯工具鏈中提供了一款資源壓縮的工具,可以通過該工具來壓縮資源,如果要啟用資源壓縮,可以在build.gradle文件中將shrinkResources true。例如:
android {...buildTypes {release {shrinkResources trueminifyEnabled trueproguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'}} }需要注意的是目前資源壓縮器目前不會移除values/文件夾中定義的資源(例如字符串、尺寸、樣式和顏色),有關(guān)詳情,請參閱問題 70869。
Android構(gòu)建工具是通過ResourceUsageAnalyzer來檢查哪些資源是無用的,當(dāng)檢查到無用的資源時(shí)會把該資源替換成預(yù)定義的版本。詳看下面代碼片段(摘自com.android.build.gradle.tasks.ResourceUsageAnalyzer):
public class ResourceUsageAnalyzer {.../*** Whether we should create small/empty dummy files instead of actually* removing file resources. This is to work around crashes on some devices* where the device is traversing resources. See http://b.android.com/79325 for more.*/public static final boolean REPLACE_DELETED_WITH_EMPTY = true;// A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAYpublic static final byte[] TINY_PNG = new byte[] {(byte)-119, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte) 10,(byte) 26, (byte) 10, (byte) 0, (byte) 0, (byte) 0, (byte) 13,(byte) 73, (byte) 72, (byte) 68, (byte) 82, (byte) 0, (byte) 0,(byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 1,(byte) 8, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 58,(byte) 126, (byte)-101, (byte) 85, (byte) 0, (byte) 0, (byte) 0,(byte) 10, (byte) 73, (byte) 68, (byte) 65, (byte) 84, (byte) 120,(byte) -38, (byte) 99, (byte) 96, (byte) 0, (byte) 0, (byte) 0,(byte) 2, (byte) 0, (byte) 1, (byte) -27, (byte) 39, (byte) -34,(byte) -4, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 73,(byte) 69, (byte) 78, (byte) 68, (byte) -82, (byte) 66, (byte) 96,(byte)-126};public static final long TINY_PNG_CRC = 0x88b2a3b0L;// A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markerspublic static final byte[] TINY_9PNG = new byte[] {(byte)-119, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte) 10,(byte) 26, (byte) 10, (byte) 0, (byte) 0, (byte) 0, (byte) 13,(byte) 73, (byte) 72, (byte) 68, (byte) 82, (byte) 0, (byte) 0,(byte) 0, (byte) 3, (byte) 0, (byte) 0, (byte) 0, (byte) 3,(byte) 8, (byte) 6, (byte) 0, (byte) 0, (byte) 0, (byte) 86,(byte) 40, (byte) -75, (byte) -65, (byte) 0, (byte) 0, (byte) 0,(byte) 20, (byte) 73, (byte) 68, (byte) 65, (byte) 84, (byte) 120,(byte) -38, (byte) 99, (byte) 96, (byte)-128, (byte)-128, (byte) -1,(byte) 12, (byte) 48, (byte) 6, (byte) 8, (byte) -96, (byte) 8,(byte)-128, (byte) 8, (byte) 0, (byte)-107, (byte)-111, (byte) 7,(byte) -7, (byte) -64, (byte) -82, (byte) 8, (byte) 0, (byte) 0,(byte) 0, (byte) 0, (byte) 0, (byte) 73, (byte) 69, (byte) 78,(byte) 68, (byte) -82, (byte) 66, (byte) 96, (byte)-126};public static final long TINY_9PNG_CRC = 0x1148f987L;// The XML document <x/> as binary-packed with AAPTpublic static final byte[] TINY_XML = new byte[] {(byte) 3, (byte) 0, (byte) 8, (byte) 0, (byte) 104, (byte) 0,(byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 28, (byte) 0,(byte) 36, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,(byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,(byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 32, (byte) 0,(byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,(byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 1,(byte) 120, (byte) 0, (byte) 2, (byte) 1, (byte) 16, (byte) 0,(byte) 36, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,(byte) 0, (byte) 0, (byte) -1, (byte) -1, (byte) -1, (byte) -1,(byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) 0, (byte) 0,(byte) 0, (byte) 0, (byte) 20, (byte) 0, (byte) 20, (byte) 0,(byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,(byte) 0, (byte) 0, (byte) 3, (byte) 1, (byte) 16, (byte) 0,(byte) 24, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,(byte) 0, (byte) 0, (byte) -1, (byte) -1, (byte) -1, (byte) -1,(byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) 0, (byte) 0,(byte) 0, (byte) 0};public static final long TINY_XML_CRC = 0xd7e65643L;... }上面截圖中3個(gè)byte數(shù)組的定義就是資源壓縮工具為無用資源提供的預(yù)定義版本,可以看出對.png提供了TINY_PNG, 對.9.png提供了TINY_9PNG以及對.xml提供了TINY_XML的預(yù)定義版本。
資源壓縮工具的詳細(xì)使用可以參考Shrink Your Code and Resources。資源壓縮工具默認(rèn)是采用安全壓縮模式來運(yùn)行,可以通過開啟嚴(yán)格壓縮模式來達(dá)到更好的瘦身效果。
如果想知道哪些資源是無用的,可以通過資源壓縮工具的輸出日志文件${project.buildDir}/outputs/mapping/release/resources.txt來查看。如下圖所示res/layout/abc_activity_chooser_viewer.xml就是無用的,然后被預(yù)定義的版本TINY_XML所替換:
資源壓縮工具只是把無用資源替換成預(yù)定義較小的版本,那我們?nèi)绾蝿h除這些無用資源呢?通常的做法是結(jié)合資源壓縮工具的輸出日志,找到這些資源并把它們進(jìn)行刪除。但在筆者的項(xiàng)目中很多無用資源是被其它組件或第三方SDK所引入的,如果采用這種優(yōu)化方式會帶來這些SDK后期維護(hù)成本的增加,針對這種情況筆者是通過采用在resources.arsc中做優(yōu)化來解決的,詳情看下面“resources.arsc的優(yōu)化”一節(jié)的介紹。
語言資源優(yōu)化
根據(jù)App自身支持的語言版本選用合適的語言資源,例如使用了AppCompat,如果不做任何配置的話,最終APK包中會包含AppCompat中消息的所有已翻譯語言字符串,無論應(yīng)用的其余部分是否翻譯為同一語言,可以通過resConfig來配置使用哪些語言,從而讓構(gòu)建工具移除指定語言之外的所有資源。下圖是具體的配置示例:
android {...defaultConfig {...resConfigs "zh", "zh-rCN"}... }針對為不同DPI所提供的圖片也可以采用相同的策略,需要針對自身的目標(biāo)用戶和目標(biāo)設(shè)備做一定的選擇,可以參考Support Only Specific Densities來操作。有關(guān)屏幕密度的詳細(xì)信息,請參閱Screen Sizes and Densities。
對.so文件也可以采用類似的策略,比如筆者的項(xiàng)目中只保留了armeabi版本的.so文件。
resources.arsc的優(yōu)化
針對resources.arsc,筆者嘗試過的優(yōu)化手段如下:
- 開啟資源混淆;
- 對重復(fù)的資源進(jìn)行優(yōu)化;
- 對被shrinkResources優(yōu)化掉的資源進(jìn)行處理。
下面將分別對這些優(yōu)化手段進(jìn)行展開介紹。
資源混淆
在筆者另一篇《美團(tuán)Android資源混淆保護(hù)實(shí)踐》文章中介紹了采用對資源混淆的方式來保護(hù)資源的安全,同時(shí)也提到了這種方式有顯著的瘦身效果。筆者當(dāng)時(shí)是采用修改AAPT的相關(guān)源碼的方式,這種方式的痛點(diǎn)是每次升級Build Tools都要修改一次AAPT源碼,維護(hù)性較差。目前筆者采用了微信開源的資源混淆庫AndResGuard,具體的原理和使用幫助可以參考安裝包立減1M–微信Android資源混淆打包工具。
無用資源優(yōu)化
在上一節(jié)中介紹了可以通過shrinkResources true來開啟資源壓縮,資源壓縮工具會把無用的資源替換成預(yù)定義的版本而不是移除,如果采用人工移除的方式會帶來后期的維護(hù)成本,這里筆者采用了一種比較取巧的方式,在Android構(gòu)建工具執(zhí)行package${flavorName}Task之前通過修改Compiled Resources來實(shí)現(xiàn)自動去除無用資源。
具體流程如下:
- 收集資源包(Compiled Resources的簡稱)中被替換的預(yù)定義版本的資源名稱,通過查看資源包(Zip格式)中每個(gè)ZipEntry的CRC-32 checksum來尋找被替換的預(yù)定義資源,預(yù)定義資源的CRC-32定義在ResourceUsageAnalyzer,下面是它們的定義。
- 通過android-chunk-utils把resources.arsc中對應(yīng)的定義移除;
- 刪除資源包中對應(yīng)的資源文件。
重復(fù)資源優(yōu)化
目前美團(tuán)App是由各個(gè)業(yè)務(wù)團(tuán)隊(duì)共同開發(fā)完成,為了方便各業(yè)務(wù)團(tuán)隊(duì)的獨(dú)立開發(fā),美團(tuán)App進(jìn)行了平臺化改造。改造時(shí)存在很多資源文件(如:drawable、layout等)被不同的業(yè)務(wù)團(tuán)隊(duì)都拷貝到自己的Library下,同時(shí)為了避免引發(fā)資源覆蓋的問題,每個(gè)業(yè)務(wù)團(tuán)隊(duì)都會為自己的資源文件名添加前綴。這樣就導(dǎo)致了這些資源文件雖然內(nèi)容相同,但因?yàn)槊Q的不同而不能被覆蓋,最終都會被集成到APK包中,針對這種問題筆者采用了和前面“無用資源優(yōu)化”一節(jié)中描述類似的策略。
具體步驟如下:
- 通過資源包中的每個(gè)ZipEntry的CRC-32 checksum來篩選出重復(fù)的資源;
- 通過android-chunk-utils修改resources.arsc,把這些重復(fù)的資源都重定向到同一個(gè)文件上;
- 把其它重復(fù)的資源文件從資源包中刪除。
代碼片段:
variantData.outputs.each {def apFile = it.packageAndroidArtifactTask.getResourceFile();it.packageAndroidArtifactTask.doFirst {def arscFile = new File(apFile.parentFile, "resources.arsc");JarUtil.extractZipEntry(apFile, "resources.arsc", arscFile);def HashMap<String, ArrayList<DuplicatedEntry>> duplicatedResources = findDuplicatedResources(apFile);removeZipEntry(apFile, "resources.arsc");if (arscFile.exists()) {FileInputStream arscStream = null;ResourceFile resourceFile = null;try {arscStream = new FileInputStream(arscFile);resourceFile = ResourceFile.fromInputStream(arscStream);List<Chunk> chunks = resourceFile.getChunks();HashMap<String, String> toBeReplacedResourceMap = new HashMap<String, String>(1024);// 處理arsc并刪除重復(fù)資源Iterator<Map.Entry<String, ArrayList<DuplicatedEntry>>> iterator = duplicatedResources.entrySet().iterator();while (iterator.hasNext()) {Map.Entry<String, ArrayList<DuplicatedEntry>> duplicatedEntry = iterator.next();// 保留第一個(gè)資源,其他資源刪除掉for (def index = 1; index < duplicatedEntry.value.size(); ++index) {removeZipEntry(apFile, duplicatedEntry.value.get(index).name);toBeReplacedResourceMap.put(duplicatedEntry.value.get(index).name, duplicatedEntry.value.get(0).name);}}for (def index = 0; index < chunks.size(); ++index) {Chunk chunk = chunks.get(index);if (chunk instanceof ResourceTableChunk) {ResourceTableChunk resourceTableChunk = (ResourceTableChunk) chunk;StringPoolChunk stringPoolChunk = resourceTableChunk.getStringPool();for (def i = 0; i < stringPoolChunk.stringCount; ++i) {def key = stringPoolChunk.getString(i);if (toBeReplacedResourceMap.containsKey(key)) {stringPoolChunk.setString(i, toBeReplacedResourceMap.get(key));}}}}} catch (IOException ignore) {} catch (FileNotFoundException ignore) {} finally {if (arscStream != null) {IOUtils.closeQuietly(arscStream);}arscFile.delete();arscFile << resourceFile.toByteArray();addZipEntry(apFile, arscFile);}}} }通過這種方式可以有效減少重復(fù)資源對包體大小的影響,同時(shí)這種操作方式對各業(yè)務(wù)團(tuán)隊(duì)透明,也不會增加協(xié)調(diào)相同資源如何被不同業(yè)務(wù)團(tuán)隊(duì)復(fù)用的成本。
總結(jié)
上述就是我們目前在APK瘦身方面的做的一些嘗試和積累,可以根據(jù)自身情況取舍使用。當(dāng)然我們還可以采取一些按需加載的策略來減少安裝包的體積。最后提一點(diǎn),砍掉不必要的功能才是安裝包瘦身的超級大招。一個(gè)好的App的標(biāo)準(zhǔn)有很多方面,但提供盡可能小的安裝包是其中一個(gè)重要的方面,這也是對我們Android開發(fā)者人員自身的提出的基本要求,要時(shí)刻保持良好的編程習(xí)慣和對包體積敏銳的嗅覺。
參考文獻(xiàn)
- Android application package (APK)
- Zip (file format)
- Build Workflow
- Android AAPT Source Code
- Reduce APK Size
- Shrink Your Code and Resources
- Manage Your App’s Memory
- Vector Drawable
- Javassist
- ASM
- pngcrush
- pngquant
- zopflipng
- android-chunk-utils
- 安裝包立減1M–微信Android資源混淆打包工具
- 減少 APK 的大小,Android 官方這樣說
- Google I/O 2016 筆記:APK 瘦身的正確姿勢
作者簡介
建帥,Android技術(shù)專家,2015年3月加入美團(tuán),目前就職于到店餐飲技術(shù)部信息與交易技術(shù)中心。
到店餐飲技術(shù)部交易與信息技術(shù)中心,負(fù)責(zé)美團(tuán)美食用戶端業(yè)務(wù),服務(wù)于數(shù)以億計(jì)用戶,通過更好的榜單、真實(shí)的評價(jià)和完善的信息為用戶提供更好的決策支持,致力于提升用戶體驗(yàn);同時(shí)承載所有餐飲商戶端線上流量,為餐飲商戶提供多種營銷工具,提升餐飲商戶營銷效率,最終達(dá)到讓國人“Eat Better、Live Better”的美好愿景!我們的團(tuán)隊(duì)包含且不限于Android、iOS、FE、Java、PHP等技術(shù)方向,已完備覆蓋前后端技術(shù)棧。只要你來,就能點(diǎn)亮全棧開發(fā)技能樹。
總結(jié)
以上是生活随笔為你收集整理的Android App包瘦身优化实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从前,小夕种了一棵树
- 下一篇: 美团内推:java高级开发(一面+二面+