随着到家业务的发展,我们的APP开始进入每月两版的迭代节奏,节奏的加快带来的副作用便是APK体积的快速增长,这会降低用户安装意愿、影响拉新拉活以及用户的留存率;另外应用市场比如 Google Play 禁止100M的APK包上传,因此APP的瘦身对我们而言势在必行。在瘦身的过程中,我们借鉴了许多业界成熟的方案,同时也提出了一些自己的想法,本文的瘦身思路将按以下几步进行阐述:如下是我们8.11.0版本APK文件资源占比如下图:图中各个目录作用如下图:
从上图中我们可以清晰看出lib、res、assets和class.dex占了很大一部分的比重,直接影响了包的体积大小,接下来围绕这几个方面我们将着重介绍采取的瘦身及包体积监控方案。
3.1 lib瘦身
APP会存在调用native层来满足业务需求,在lib目录下包含了各种CPU支持集目录,比如armeabi、armeabi-v7a、arm64-v8a、x86和x86_64等来兼容不同的手机系统,目前arm64-v8a为市场主流版本,在项目中只需要armeabi和arm64-v8a两种来完成手机适配。3.1.1 无用so文件剔除
随着版本不断更新迭代,项目中引用的一些第三方或本地编写的so文件会存在一些冗余,因此我们通过脚本定期扫描去除无用so文件。针对于市面上的Android手机使用CPU系统的不同,在项目中就需要引入多套CPU支持集,这会导致so文件体积成倍的增加;而用户安装的APP只会用到对应手机系统CPU支持集下的so文件资源,而其它CPU支持集对用户来说是用不到的,这在流量及资源上是一种浪费。针对这个问题,我们根据CPU支持集进行拆分打包成多个APK。实施方案:在module项目build.gradle文件中通过脚本动态控制ndk指定单一架构的CPU支持集进行输出:ndk { abiFilters "armeabi"或"arm64-v8a" }
根据上图可看出拆分前后APK文件相差可达20+M,用户如何无感下载对应CPU系统APP:- 应用市场:通过双包上传至应用市场平台,平台会根据用户手机系统下载对应APK文件;
- APP站内升级:获取手机CPU支持集下载对应APK文件;
在整体的APK包占比中图片以及文件资源占比达到了25%以上,通过对资源的归类以及问题的排查,我们制定了如下图所示的资源瘦身方案:3.2.1 图片资源
图片资源是资源瘦身占比中最重要的一环,在模块化开发过程中会存在研发人员使用图片规范不统一及图片未压缩的问题,导致项目中图片资源占比越来越大,因此我们着重从以下两个方面解决问题:图片使用规范:
- 图片适配:Android为了适配不同分辨率的设备在资源目录下通常会存放1X、2X、3X等图片资源,但事实上我们只需要选择1X或者2X即可。
- 图片格式优化:总体上遵循无透明通道使用JPG,优先使用Webp以及通过.9和矢量图实现图片拉伸替代大图。
- 着色方案:对于一些颜色单一的图片通过代码去实现替代图片。
针对于图片未压缩导致图片资源增大的情况,通过TinyPNG将图片转换为更小的8-bit图片。由于模块化及压缩步骤繁琐导致人员成本及错误率增加,我们采用脚本进行批量自动压缩,达到了一键瘦身的同时对后续开发中图片的引用也起到规范及常态化管理的目的。压缩配置策略:
TinyPngInfo{
api_key=["xxxxxxx",
"xxxxxxx",
"xxxxxxx"]
checkModuleName=['xx']
excludeImage=['xx.png']
checkResMinSize=0
compressImage=[]
checkResDir=["src${File.separator}main${File.separator}assets",
"src${File.separator}main${File.separator}res"]
}
脚本流程图:
执行结果:
我们知道Proguard能混淆Java代码,但混淆不了资源文件目录,AndResGuard 弥补了这个空缺。其作用有两点:- 通过混淆资源ID长度减小了APK包大小,例如将res/drawable/hello.png混淆为r/s/a.png,并将映射关系输出到mapping文件中;
- 混淆后在安全性方面有一点提升,提高了逆向破解难度;
实现原理参照下图:
通过该工具res目录从5.7M瘦身到4.7M,占res资源17%,瘦身效果显著。
3.2.3 APK压缩格式优化
首先为什么要进行压缩格式的优化呢?通过分析APK的打包流程我们发现如下代码片段:
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"
};
可以看出在构建APK的过程中,zip压缩模式下这些资源是不会被打包压缩的。因此我们考虑更换压缩格式的方式去实现更小的APK,通过调研发现7zip在同等体积下,压缩率比zip高5%左右,因此我们最终选择使用7zip去压缩文件。具体的实现方式是在加固后签名前的时机启用7zip压缩功能,通过脚本工具完成压缩及最终产物的生成;通过脚本压缩签名后的APK文件以32位APK为例,普通APK包大小为50.4M,启用7zip压缩后APK包大小为48.2M,可减少2.2M,占到总体积大小的5%。由于项目的快速迭代以及模块化开发,项目中不可避免的出现了大量的重复以及无用资源;通过调研我们采用了腾讯开源的ApkChecker。它是如何排查重复和无用资源呢?
- 针对重复资源,它是通过遍历解压后的所有文件夹,对每一个文件内容求MD5值,MD5值相同即为重复文件。
- 针对无用资源,简单来说它会通过读取R.txt获取所有的资源,然后遍历所有的资源文件探寻引用关系,最终排查出无用的资源。
3.3 类文件优化
从APK资源构成中,我们发现classes.dex占比30%左右,且随着业务迭代占比会越来越高。dex文件本质上从class文件演变而来,针对移动端产生的一种新的类文件格式,我们日常开发的业务代码以及第三方SDK最终都会被编译成dex文件。如何优化dex文件呢?- 保持良好的代码习惯,慎用第三方库,选择体积较小的第三方库,剔除掉重复以及无用的代码(注意一定要case by case )。
- 开启ProGuard来进行类文件压缩,开启之后便可以对代码进行混淆、优化、压缩等工作。
- 代码缩减(即摇树优化):从应用及其库依赖项中检测并安全地移除不使用的类、字段、方法和属性(这使其成为了一个对于规避 64k 引用限制非常有用的工具)。例如,如果您仅使用某个库依赖项的少数几个 API,那么缩减功能可以识别应用不使用的库代码并仅从应用中移除这部分代码。如需了解详情,请参照文末文献。
- 从封装应用中移除不使用的资源,包括应用库依赖项中不使用的资源。此功能可与代码缩减功能结合使用,这样一来,移除不使用的代码后,也可以安全地移除不再引用的所有资源。如需了解详情,请参照文末文献。
- 缩短类和成员的名称,从而减小dex文件的大小。如需了解详情,请参照文末文献。
- 优化:检查并重写代码,以进一步减小应用的dex文件的大小。
android{
...
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
- jni方法不可混淆,方法名需与native方法保持一致;
- 四大组件、Application子类、Framework层下的类、自定义的View默认不会被混淆,无需另外配置。
WebView的JS调用接口方法不可混淆;
注解相关的类不混淆;
Bean数据类不可混淆;
枚举enum类中的values和valuesof这两个方法不可混淆(反射调用);
继承Parceable和Serializable等可序列化的类不可混淆;
第三方库或SDK,请参考第三方提供的混淆规则,没提供的话,建议第三方包全部不混淆;
有了瘦身方案,保证项目不反弹也很重要,但是如果每次都依赖组内成员的自查以及团队成员内部的review,需要花费的时间成本可想而知。因此我们做了版本增量对比工具,该工具主要实现了以下功能:- 多维度监控:支持单个文件超限配置、模块文件增量超限配置、APK整体增量超限配置三个维度的版本增量控制。
- 结果呈现直观:对比结果从整体、新增、删除、变大、变小五个维度直观排列,更易排查版本增量问题。
使用方式:
allResCheckInfo {
remoteVersion = '1.1.0'
versionName = '1.0.0'
warnSize = 5
outPutsAddSize = 100
outPutsSize = 50000
sortAllFileSize = true
check_projectNames=['app','jni']
outPutsFileName = ""
projectName = ''
check_userName = '****'
check_passWord = '****'
serviceUrl="http://****"
isOpenPushRemote = true
}
工作流程如下图:
5.1 瘦身成果
通过以上的措施我们的APP在整个一年内从版本8.9.0的48.43M经历过24个版本的迭代到现在8.21.5的48M,整体一直维持在可控的范围内。同时在接下来的版本迭代中,我们会将APK瘦身常态化,始终维持包体积在可控的范围内。 5.2 后续规划
APP瘦身是一个持续不断的过程,未来我们将在以下几个方面做重点探索:https://github.com/shwenzhang/AndResGuardMatrix-ApkCheckerhttps://github.com/Tencent/matrix/wiki/Matrix-Android-ApkCheckerhttps://developer.android.google.cn/studio/build/shrink-code#shrink-resourceshttps://tinypng.com/developershttps://mp.weixin.qq.com/s/JgNugWIyor23J-nrKMC6dg