Android App包瘦身优化实践

随着业务的快速迭代增长,美团App里不断引入新的业务逻辑代码、图片资源和第三方SDK,直接导致APK体积不断增长。包体积增长带来的问题越来越多,如CDN流量费用增加、用户安装成功率降低,甚至可能会影响用户的留存率。APK的瘦身已经是不得不考虑的事情。在尝试瘦身的过程中,我们借鉴了很多业界其他公司提供的方案,同时也针对自身特点,发现了一些新的技巧。本文将对其中的一些做详细介绍。

在开始讲瘦身技巧之前,先来讲一下APK的构成。

APK的构成

可以用Zip工具打开APK查看。比如,美团App 7.8.6的线上版本的格式是这样的:

可以看到APK由以下主要部分组成:

文件/目录描述
lib/存放so文件,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持armabi与x86的架构即可,如果非必需,可以考虑拿掉x86的部分
res/存放编译后的资源文件,例如:drawable、layout等等
assets/应用程序的资源,应用程序可以使用AssetManager来检索该资源
META-INF/该文件夹一般存放于已经签名的APK中,它包含了APK中所有文件的签名摘要等信息
classes(n).dexclasses文件是Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式
resources.arsc编译后的二进制资源文件
AndroidManifest.xmlAndroid的清单文件,格式为AXML,用于描述应用程序的名称、版本、所需权限、注册的四大组件

当然还会有一些其它的文件,例如上图中的org/src/push_version等文件或文件夹。这些资源是Java Resources,感兴趣的可以结合编译工作流中的流程图以及MergeJavaResourcesTransform的源码看看被打入APK包中的资源都有哪些,这里不做过多介绍。

在充分了解了APK各个组成部分以及它们的作用后,我们针对自身特点进行了分析和优化。下面将从Zip文件格式、classes.dex、资源文件、resources.arsc等方面来介绍下我们发现的部分优化技巧。

Zip格式优化

前面介绍了APK的文件格式以及主要组成部分,通过aapt l -v xxx.apkunzip -l xxx.apk来查看APK文件时会得到以下信息,见下面截图:

通过上图可以看到APK中很多资源是以Stored来存储的,根据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在资源处理时对这些文件后缀类型的资源是不做压缩的,那是不是可以修改它们的压缩方式从而达到瘦身的效果呢?

在介绍怎么做之前,先来大概介绍一下App的资源是怎么被打进APK包里的。Android构建工具链使用AAPT工具来对资源进行处理,来看下图(图片来源于Build Workflow):

通过上图可以看到ManifestResourcesAssets的资源经过AAPT处理后生成R.javaProguard ConfigurationCompiled Resources。其中R.java大家都比较熟悉,这里就不过多介绍了。我们来重点看看Proguard ConfigurationCompiled Resources都是做什么的呢?

  • Proguard Configuration是AAPT工具为Manifest中声明的四大组件以及布局文件中(XML layouts)使用的各种Views所生成的ProGuard配置,该文件通常存放在${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/proguard-rules/${flavorName}/${buildType}/aapt_rules.txt,下面是项目中该文件的截图,红框标记出来的就是对AndroidManifest.xmlXML Layouts中相关Class的ProGuard配置。

  • Compiled Resources是一个Zip格式的文件,这个文件的路径通常为${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/resources-${flavorName}-${buildType}-stripped.ap_。 通过下面经过Zip解压后的截图,可以看出这个文件包含了resAndroidManifest.xmlresources.arsc的文件或文件夹。结合Build Workflow中的描述,可以看出这个文件(resources-${flavorName}-${buildType}-stripped.ap_)会被apkbuilder打包到APK包中,它其实就是APK的“资源包”(resAndroidManifest.xmlresources.arsc)。

我们就是通过这个文件来修改不同后缀文件资源的压缩方式来达到瘦身效果的,而在后面“resources.arsc的优化”一节中也是操作的这个文件。

笔者在自己的项目中是通过在package${flavorName} Task(感兴趣的同学可以查看源码)之前进行这个操作的。

下面是部分代码片段:

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);}}
}

当然也可以在其它构建步骤中采用更高压缩率的方式来达到瘦身效果,例如采用7Zip压缩等等。

本技巧的使用需要注意以下问题: * 如果音视频资源被压缩存放在APK中的话,在使用一些音频、视频API时尤其要注意,需要做好充分的测试。 * resources.arsc文件最好不要压缩存储,如果压缩会影响一定的性能(尤其是冷启动时间)。 * 如果想在Android 6.0上开启android:extractNativeLibs=”false”的话,.so 文件也不能被压缩,android:extractNativeLibs的使用姿势看这里:App Manifest — application。

classes.dex的优化

如何优化classes.dex的大小呢?大体有如下套路:

  • 时刻保持良好的编程习惯和对包体积敏锐的嗅觉,去除重复或者不用的代码,慎用第三方库,选用体积小的第三方SDK等等。
  • 开启ProGuard来进行代码压缩,通过使用ProGuard来对代码进行混淆、优化、压缩等工作。

针对第一种套路,因各个公司的项目的差异,共性的东西较少,需要case by case的分析,这里不做过多的介绍。

压缩代码

可以通过开启ProGuard来实现代码压缩,可以在build.gradle文件相应的构建类型中添加minifyEnabled true

请注意,代码压缩会拖慢构建速度,因此应该尽可能避免在调试构建中使用。不过一定要为用于测试的最终APK启用代码压缩,如果不能充分地自定义要保留的代码,可能会引入错误。

例如,下面这段来自build.gradle文件的代码用于为发布构建启用代码压缩:

android {buildTypes {release {minifyEnabled trueproguardFiles getDefaultProguardFile(‘proguard-android.txt'),'proguard-rules.pro'}}...
}

除了minifyEnabled属性外,还有用于定义ProGuard规则的proguardFiles属性:

  • getDefaultProguardFile(‘proguard-android.txt')是从Android SDKtools/proguard/文件夹获取默认ProGuard设置。
  • proguard-rules.pro文件用于添加自定义ProGuard规则。默认情况下,该文件位于模块根目录(build.gradle文件旁)。

提示:要想做进一步的代码压缩,可尝试使用位于同一位置的proguard-android-optimize.txt文件。它包括相同的ProGuard规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小APK大小和帮助提高其运行速度。

在Gradle Plugin 2.2.0及以上版本ProGuard的配置文件会自动解压缩到${rootProject.buildDir}/${AndroidProject.FD_INTERMEDIATES}/proguard-files/目录下,proguardFiles会从这个目录来获取ProGuard配置。

每次执行完ProGuard之后,ProGuard都会在${project.buildDir}/outputs/mapping/${flavorDir}/生成以下文件:

文件名描述
dump.txtAPK中所有类文件的内部结构
mapping.txt提供原始与混淆过的类、方法和字段名称之间的转换,可以通过proguard.obfuscate.MappingReader来解析
seeds.txt列出未进行混淆的类和成员
usage.txt列出从APK移除的代码

可以通过在usage.txt文件中看到哪些代码被删除了,如下图中所示android.support.multidex.MultiDex已经被删除了:

R Field的优化

除了对项目代码优化和开启代码压缩之外,笔者在《美团Android DEX自动拆包及动态加载简介》这篇文章中提到了通过内联R Field来解决R Field过多导致MultiDex 65536的问题,而这一步骤对代码瘦身能够起到明显的效果。下面是笔者通过字节码工具在构建流程中内联R Field的代码片段(字节码的修改可以使用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) {}}
}

其它优化手段

针对代码的瘦身还有很多优化的技巧,例如:

  • 减少ENUM的使用(详情可以参考:Remove Enumerations),每减少一个ENUM可以减少大约1.0到1.4 KB的大小;
  • 通过pmd cpd来检查重复的代码从而进行代码优化;
  • 移除掉所有无用或者功能重复的依赖库。

这些优化技巧就不展开介绍了。

资源的优化

图片优化

为了支持Android设备DPI的多样化([l|m|tv|h|x|xx|xxx]dpi)以及用户对高质量UI的期待,美团App中使用了大量的图片,在Android下支持很多格式的图片,例如:PNG、JPG 、WebP,那我们该怎么选择不同类型的图片格式呢? 在Google I/O 2016中提到了针对图片格式的选择,来看下图(图片来源于Image compression for Android developers):

通过上图可以看出一个大概图片格式选择的方法。如果能用VectorDrawable来表示的话优先使用VectorDrawable,如果支持WebP则优先用WebP,而PNG主要用在展示透明或者简单的图片,而其它场景可以使用JPG格式。针对每种图片格式也有各类的优化手段和优化工具。

使用矢量图片

可以使用矢量图形来创建独立于分辨率的图标和其他可伸缩图片。使用矢量图片能够有效的减少App中图片所占用的大小,矢量图形在Android中表示为VectorDrawable对象。 使用VectorDrawable对象,100字节的文件可以生成屏幕大小的清晰图像,但系统渲染每个VectorDrawable对象需要大量的时间,较大的图像需要更长的时间才能出现在屏幕上。 因此只有在显示小图像时才考虑使用矢量图形。有关使用VectorDrawable的更多信息,请参阅 Working with Drawables。

使用WebP

如果App的minSdkVersion高于14(Android 4.0+)的话,可以选用WebP格式,因为WebP在同画质下体积更小(WebP支持透明度,压缩比比JPEG更高但显示效果却不输于JPEG,官方评测quality参数等于75均衡最佳), 可以通过PNG到WebP转换工具来进行转换。当然Android从4.0才开始WebP的原生支持,但是不支持包含透明度,直到Android 4.2.1+才支持显示含透明度的WebP,在笔者使用中是判断当前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)}}}
}   

选择更优的压缩工具

可以使用pngcrush、pngquant或zopflipng等压缩工具来减少PNG文件大小,而不会丢失图像质量。所有这些工具都可以减少PNG文件大小,同时保持图像质量。

pngcrush工具特别有效:此工具在PNG过滤器和zlib(Deflate)参数上迭代,使用过滤器和参数的每个组合来压缩图像。然后选择产生最小压缩输出的配置。

对于JPEG文件,你可以使用packJPG或guetzli等工具将JPEG文件压缩的更小,这些工具能够在保持图片质量不变的情况下,把图片文件压缩的更小。guetzli工具更是能够在图片质量不变的情况下,将文件大小降低35%。

在Android构建流程中AAPT会使用内置的压缩算法来优化res/drawable/目录下的PNG图片,但也可能会导致本来已经优化过的图片体积变大,可以通过在build.gradle中设置cruncherEnabled来禁止AAPT来优化PNG图片。

aaptOptions {cruncherEnabled = false
}

开启资源压缩

Android的编译工具链中提供了一款资源压缩的工具,可以通过该工具来压缩资源,如果要启用资源压缩,可以在build.gradle文件中将shrinkResources true。例如:

android {...buildTypes {release {shrinkResources trueminifyEnabled trueproguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'}}
}

需要注意的是目前资源压缩器目前不会移除values/文件夹中定义的资源(例如字符串、尺寸、样式和颜色),有关详情,请参阅问题 70869。

Android构建工具是通过ResourceUsageAnalyzer来检查哪些资源是无用的,当检查到无用的资源时会把该资源替换成预定义的版本。详看下面代码片段(摘自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个byte数组的定义就是资源压缩工具为无用资源提供的预定义版本,可以看出对.png提供了TINY_PNG, 对.9.png提供了TINY_9PNG以及对.xml提供了TINY_XML的预定义版本。

资源压缩工具的详细使用可以参考Shrink Your Code and Resources。资源压缩工具默认是采用安全压缩模式来运行,可以通过开启严格压缩模式来达到更好的瘦身效果。

如果想知道哪些资源是无用的,可以通过资源压缩工具的输出日志文件${project.buildDir}/outputs/mapping/release/resources.txt来查看。如下图所示res/layout/abc_activity_chooser_viewer.xml就是无用的,然后被预定义的版本TINY_XML所替换:

资源压缩工具只是把无用资源替换成预定义较小的版本,那我们如何删除这些无用资源呢?通常的做法是结合资源压缩工具的输出日志,找到这些资源并把它们进行删除。但在笔者的项目中很多无用资源是被其它组件或第三方SDK所引入的,如果采用这种优化方式会带来这些SDK后期维护成本的增加,针对这种情况笔者是通过采用在resources.arsc中做优化来解决的,详情看下面“resources.arsc的优化”一节的介绍。

语言资源优化

根据App自身支持的语言版本选用合适的语言资源,例如使用了AppCompat,如果不做任何配置的话,最终APK包中会包含AppCompat中消息的所有已翻译语言字符串,无论应用的其余部分是否翻译为同一语言,可以通过resConfig来配置使用哪些语言,从而让构建工具移除指定语言之外的所有资源。下图是具体的配置示例:

android {...defaultConfig {...resConfigs "zh", "zh-rCN"}...
}    

针对为不同DPI所提供的图片也可以采用相同的策略,需要针对自身的目标用户和目标设备做一定的选择,可以参考Support Only Specific Densities来操作。有关屏幕密度的详细信息,请参阅Screen Sizes and Densities。

.so文件也可以采用类似的策略,比如笔者的项目中只保留了armeabi版本的.so文件。

resources.arsc的优化

针对resources.arsc,笔者尝试过的优化手段如下:

  • 开启资源混淆;
  • 对重复的资源进行优化;
  • 对被shrinkResources优化掉的资源进行处理。

下面将分别对这些优化手段进行展开介绍。

资源混淆

在笔者另一篇《美团Android资源混淆保护实践》文章中介绍了采用对资源混淆的方式来保护资源的安全,同时也提到了这种方式有显著的瘦身效果。笔者当时是采用修改AAPT的相关源码的方式,这种方式的痛点是每次升级Build Tools都要修改一次AAPT源码,维护性较差。目前笔者采用了微信开源的资源混淆库AndResGuard,具体的原理和使用帮助可以参考安装包立减1M–微信Android资源混淆打包工具。

无用资源优化

在上一节中介绍了可以通过shrinkResources true来开启资源压缩,资源压缩工具会把无用的资源替换成预定义的版本而不是移除,如果采用人工移除的方式会带来后期的维护成本,这里笔者采用了一种比较取巧的方式,在Android构建工具执行package${flavorName}Task之前通过修改Compiled Resources来实现自动去除无用资源。

具体流程如下:

  • 收集资源包(Compiled Resources的简称)中被替换的预定义版本的资源名称,通过查看资源包(Zip格式)中每个ZipEntryCRC-32 checksum来寻找被替换的预定义资源,预定义资源的CRC-32定义在ResourceUsageAnalyzer,下面是它们的定义。
  	// A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAYpublic static final long TINY_PNG_CRC = 0x88b2a3b0L;// A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markerspublic static final long TINY_9PNG_CRC = 0x1148f987L;// The XML document <x/> as binary-packed with AAPTpublic static final long TINY_XML_CRC = 0xd7e65643L;
  • 通过android-chunk-utils把resources.arsc中对应的定义移除;
  • 删除资源包中对应的资源文件。

重复资源优化

目前美团App是由各个业务团队共同开发完成,为了方便各业务团队的独立开发,美团App进行了平台化改造。改造时存在很多资源文件(如:drawable、layout等)被不同的业务团队都拷贝到自己的Library下,同时为了避免引发资源覆盖的问题,每个业务团队都会为自己的资源文件名添加前缀。这样就导致了这些资源文件虽然内容相同,但因为名称的不同而不能被覆盖,最终都会被集成到APK包中,针对这种问题笔者采用了和前面“无用资源优化”一节中描述类似的策略。

具体步骤如下:

  • 通过资源包中的每个ZipEntryCRC-32 checksum来筛选出重复的资源;
  • 通过android-chunk-utils修改resources.arsc,把这些重复的资源都重定向到同一个文件上;
  • 把其它重复的资源文件从资源包中删除。

代码片段:

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并删除重复资源Iterator<Map.Entry<String, ArrayList<DuplicatedEntry>>> iterator = duplicatedResources.entrySet().iterator();while (iterator.hasNext()) {Map.Entry<String, ArrayList<DuplicatedEntry>> duplicatedEntry = iterator.next();// 保留第一个资源,其他资源删除掉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);}}}
}

通过这种方式可以有效减少重复资源对包体大小的影响,同时这种操作方式对各业务团队透明,也不会增加协调相同资源如何被不同业务团队复用的成本。

总结

上述就是我们目前在APK瘦身方面的做的一些尝试和积累,可以根据自身情况取舍使用。当然我们还可以采取一些按需加载的策略来减少安装包的体积。最后提一点,砍掉不必要的功能才是安装包瘦身的超级大招。一个好的App的标准有很多方面,但提供尽可能小的安装包是其中一个重要的方面,这也是对我们Android开发者人员自身的提出的基本要求,要时刻保持良好的编程习惯和对包体积敏锐的嗅觉。

参考文献

  • 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技术专家,2015年3月加入美团,目前就职于到店餐饮技术部信息与交易技术中心。

到店餐饮技术部交易与信息技术中心,负责美团美食用户端业务,服务于数以亿计用户,通过更好的榜单、真实的评价和完善的信息为用户提供更好的决策支持,致力于提升用户体验;同时承载所有餐饮商户端线上流量,为餐饮商户提供多种营销工具,提升餐饮商户营销效率,最终达到让国人“Eat Better、Live Better”的美好愿景!我们的团队包含且不限于Android、iOS、FE、Java、PHP等技术方向,已完备覆盖前后端技术栈。只要你来,就能点亮全栈开发技能树。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/478358.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NLP界新SOTA!吸纳5000万级知识图谱,一举刷爆54个中文任务!

大家还记得2019年底首次将GLUE榜单分数刷过90大关的百度ERNIE模型吗&#xff1f;在随后一年多的时间里&#xff0c;又陆续出现了GPT-3、Switch Transformer等一众靠模型体量取胜的千亿乃至万亿参数规模的超大预训练模型&#xff0c;似乎新训一个预训练模型没有个千亿参数都不好…

LeetCode 543. 二叉树的直径(DFS)

1. 题目 给定一棵二叉树&#xff0c;你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。 示例 : 给定二叉树1/ \2 3/ \ 4 5 返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。注意&#xff1a;两结点之…

论文浅尝 | 用于嵌套命名实体识别的二部平面图网络(BiFlaG)

笔记整理 | 栾岱洋&#xff0c;东南大学来源&#xff1a;ACL 2020链接&#xff1a;https://www.aclweb.org/anthology/2020.acl-main.571.pdf本文提出了一种可以实现重叠主体的识别的BiFlaG&#xff08;bipartite flatgraph network&#xff09;模型&#xff0c;包含两个子图模…

不是所有问题都适合用神经网络去搞!

文 | YukiRain知乎不是所有问题都适合用神经网络预测&#xff0c;YukiRain总结了以下几种不适合用神经网络做预测的场景&#xff1a;小样本情况&#xff0c;无论是低维还是高维&#xff0c;不如SVM和贝叶斯模型低维数据&#xff0c;大样本量&#xff0c;不如各种ensemble类算法…

从Google白皮书看企业安全最佳实践

前不久Google发布了一份安全方面的白皮书Google Infrastructure Security Design Overview&#xff0c;直译的版本可以参考“网路冷眼”这版《Google基础设施安全设计概述》&#xff0c;直译点评的版本可以参考“职业欠钱”的《Google基础设施安全设计概述翻译和导读》。 此前G…

LeetCode 415. 字符串相加(大数加法)

1. 题目 给定两个字符串形式的非负整数 num1 和num2 &#xff0c;计算它们的和。 注意&#xff1a;num1 和num2 的长度都小于 5100. num1 和num2 都只包含数字 0-9. num1 和num2 都不包含任何前导零。 你不能使用任何內建 BigInteger 库&#xff0c; 也不能直接将输入的字符串…

会议交流 | 智能风控技术峰会(请关注图分析相关论坛)

2021年5月15日&#xff0c;9:00-18:00&#xff0c;DataFunSummit——智能风控技术峰会将如约而至&#xff0c;本次峰会由3位主席和8位出品人精心策划而成&#xff0c;邀请来自业界的40余位嘉宾从风控系统的架构&#xff0c;到智能风控的核心算法&#xff0c;再到内容、金融、交…

深度学习,路在何方?

文 | Severus最近&#xff0c;AI领域的三位图灵奖获得者Yoshua Bengio、Yann LeCun和Geoffrey Hinton共同发表了一篇文章&#xff0c;名为Deep Learning for AI&#xff0c;文中讨论了深度学习的起源、发展、成就及未来。文章标题&#xff1a;Deep Learning for AI原文链接&…

前端渲染引擎doT.js解析

背景 前端渲染有很多框架&#xff0c;而且形式和内容在不断发生变化。这些演变的背后是设计模式的变化&#xff0c;而归根到底是功能划分逻辑的演变&#xff1a;MVC—>MVP—>MVVM&#xff08;忽略最早混在一起的写法&#xff0c;那不称为模式&#xff09;。近几年兴起的R…

elasticsearch7使用指导

目录结构&#xff1a; 一、es概述 二、es安装/head插件安装/kibana安装 三、es核心概念 四、IK分词器 五、RestFul操作 六、CRUD(增删改查) 七、Springboot集成es ---------------------------------------分割线&#xff1a;正文------------------------------------…

开源开放 | 欢迎选修浙江大学《知识图谱》开放共享慕课

点击“阅读原文”或扫描图中二维码进入课程教学计划第一章知识图谱概论1.1 语言与知识1.2 知识图谱的起源1.3 知识图谱的价值1.4 知识图谱的技术内涵第二章知识图谱的表示2.1 什么是知识表示2.2 人工智能历史发展长河中的知识表示2.3 知识图谱的符号表示方法2.4 知识图谱的向量…

LeetCode 43. 字符串相乘(大数乘法)

文章目录1. 题目2. 小学竖式乘法2.1 普通版2.2 优化版1. 题目 给定两个以字符串形式表示的非负整数 num1 和 num2&#xff0c;返回 num1 和 num2 的乘积&#xff0c;它们的乘积也表示为字符串形式。 示例 1: 输入: num1 "2", num2 "3" 输出: "6&q…

旅游推荐系统的演进

度假业务在整个在线旅游市场中占据着非常重要的位置&#xff0c;如何做好做大这块蛋糕是行业内的焦点。与美食或酒店的用户兴趣点明确&#xff08;比如找某个确定的餐厅或者找某个目的地附近的酒店&#xff09;不同&#xff0c;旅游场景中的用户兴趣点&#xff08;比如周末去哪…

预训练卷不动,可以卷输入预处理啊!

文 | 德志编 | 小戏目前伴随着预训练预言模型的兴起&#xff0c;越来越多的 NLP 任务开始脱离对分词的依赖。通过 Fine-Tune Bert 这类预训练预言模型&#xff0c;能直接在下游任务上取得一个很好的结果。同时也有文章探讨中文分词在神经网络时代的必要性。对于分词任务本身也是…

检索式问答以及评论观点抽取+情感分析

3款开发者神器&#xff0c;快速搭建「检索、问答、情感分析」应用&#xff01; 人工智能与算法学习 于 2021-12-30 08:10:00 发布 26 收藏 文章标签&#xff1a; 百度 大数据 机器学习 人工智能 数据分析 原文链接&#xff1a;https://mp.weixin.qq.com/s?__bizMzIyOTkyOTE…

征稿 | Call for papers on Knowledge Graphs

Knowledge graph是Data Intelligence的核心主题和期刊特色之一。为持续展示这一领域的最新进展和前沿成果&#xff0c;Data Intelligence正在与国际学者一道策划两期Knowledge graph专辑。期待大家关注并积极投稿参与&#xff01;DI专辑Special Issue on Personal Health Knowl…

LeetCode 179. 最大数(自定义谓词函数--Lambda表达式--排序)

1. 题目 给定一组非负整数&#xff0c;重新排列它们的顺序使之组成一个最大的整数。 示例 1: 输入: [10,2] 输出: 210示例 2: 输入: [3,30,34,5,9] 输出: 9534330 说明: 输出结果可能非常大&#xff0c;所以你需要返回一个字符串而不是整数。来源&#xff1a;力扣&#xff08…

python实现requests访问接口,比如es接口

首先我们先引入requests模块 import requests一、发送请求 r requests.get(https://api.github.com/events) # GET请求 r requests.post(http://httpbin.org/post, data {key:value}) # POST请求 r requests.put(http://httpbin.org/put, data {key:value}) # PUT请求 r…

拿下字节offer,这些面试题命中率高达90%以上

昨天在知乎上刷到一个热门问题:程序员需要达到什么水平才能顺利拿到 20k 无压力&#xff1f;其中一个最热门的回答是&#xff1a;“其实&#xff0c;无论你是前端还是后端、想进大厂还是拿高薪&#xff0c;算法都一定很重要。”为什么&#xff0c;算法会如此重要&#xff1f;不…

HDFS NameNode重启优化

本文已发表于InfoQ&#xff0c;下面的版本又经过少量修订。 一、背景 在Hadoop集群整个生命周期里&#xff0c;由于调整参数、Patch、升级等多种场景需要频繁操作NameNode重启&#xff0c;不论采用何种架构&#xff0c;重启期间集群整体存在可用性和可靠性的风险&#xff0c;所…