本文转自Android安装包瘦身详解
前言
前段时间跟我朋友讨论起安装包的瘦身问题,故此打算一篇结合过往项目经验安装包瘦身的总结,分享一些了解到的技巧. 不少朋友反映在wifi相当普遍的今天,而且5G都快要落地了,是否还有必要为安装包瘦身呢? 我的答案是肯定的.
谷歌在2018的IO大会上,透露了一张谷歌play上安装包体积和下载转化率的关系图:

转化率
从上面图中不难看出体积越小转化率越高.试想下当用户想下载你APP的时候,即使点了,也有可能因为网络缓慢而反悔取消.但如果你APP体积小,可能用户还在犹豫的时候已经下载好了.
推广成本
安装包的体积跟地推成本和手机厂商的预安装费都有着直接影响.特别是厂商预装,这主要是因为厂商留给 预装应用的总空间是有限的.如果你的包体积非常大,那就会影响厂商预装其他应用.
应用市场
不少应用市场对安装包的体积是有限制的,以谷歌play为例,它就对超过100M的应用上传方式做了限制,原因是包体积过大会对应用市场的服务器带宽成本造成压力
应用性能
安装包体积除了以上有影响外,对应用的性能也有这直接的影响.
- 安装时间: 文件拷⻉、Library解压、编译ODEX、签名校验,特别对于Android 5、6系统来说,如果你的APP有多个Dex,那光是编译ODEX的时间可能长达几分钟.
- 运行内存: 项目中的Resource资源、Library以及Dex类不作优化的加载到内存时,这些都会占用不少空间.
- ROM空间: 如果应用的APK达到了100多MB那么解压出来后就可能接近300MB了,对于几年前的16G千元机来说,压力是相当大的, 很可能因为闪存不足造成写入放大
综上所述安装包的体积也是项目的一个重要技术指标,那该如何为应用瘦身呢?
安装包体积优化

资源优化
1. SVG 配合 Tint 替换icon图片资源套图
-
先从icon入手,在实际项目中一般可以让UI把png jpg的icon换成SVG格式的矢量图,然后再用AS转换下格式可以直接在Android上跑了.这样就可以用一个SVG文件即可代替多个不同分别率的图片资源.
详细使用可以参考Android SVG、 SVG图片下载地址 -
但如果同一个icon不同颜色怎么办呢?例如点击效果、不同业务状态等情况 这时候只需要配合Tint属性使用即可.不用多个图片资源文件
详细使用可以参考SVG-Android开源库——图片颜色Manage、Tint及Selector扩展
2. Webp 和 图片压缩
-
资源有大张的图片时,如背景等,可以考虑将资源图片转换成webp格式
可以参考Android Webp 完全解析 快来缩小apk的大小吧
3. 去除多余的国际化代码
- 一般第三方包都会带上比较多的国际化配置,而我们的应用往往只需要在国内跑或者适配中英文即可.其他资源的配置就成多余的代码.这时候我们就应该通过gradle配置把多余的国际化配置去掉
详细参考使用resConfigs去除无用语言资源
4. NDK库兼容
- 一般NDK库都会分了多个包 如arm64-v8a、armeabi-v7a、armeabi、x86、mips64、mip等.其实大部分手机项目只需兼容armeabi系列的包,而NDK库一般还会向下兼容.
如armeabi-v7a设备兼容armeabi-v7a、armeabi
所以正常情况只保留armeabi-v7a即可.不过最好咨询下SDK的技术人员再作配置.
详细参考so库兼容
5. 去除无用资源
- 第一阶段:Lint
从Eclipse开始,就开发工具就支持Lint这个静态代码扫描工具,它里面就支持Unused Resources扫描.然后我们直接选择"Remove All Unused Resources",就可以轻松删除无用资源。既然它是第一阶段方案,那Lint 方案扫描缺点是什么呢?
Lint作为一个静态扫描工具,它最大的问题在于没有考虑到ProGuard的代码裁剪。在ProGuard过程我们会shrink掉大量的无用 代码,但是Lint工具并不能检查出这些无用代码所引用的无用资源.
可以参考美团外卖Android Lint代码检查实践
- 第二阶段:shrinkResources
所以Android在第二阶段增加了“shrinkResources”资源压缩功能,它需要配合ProGurad的“minifyEnabled”功能同时使用. 如果ProGuard把部分无用代码移除,这些代码所引用的资源也会被标记为无用资源,然后通过资源压缩功能将它们移除.
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
}
}
}
看起来不错,但是目前的shrinkResources实现起来还有几个缺陷.
-
没有处理resources.arsc文件: 这样导致大量无用的String、ID、Attr、Dimen等资源并没有被删除.
-
没有真正删除资源文件: 对于Drawable、Layout这些无用资源,shrinkResources也没有真正把它们删掉,而是仅仅替换为 一个空文件.
所以尽管我们的应用有大量的无用资源,但是系统目前的做法并没有真正减少文件数量。这样resources.arsc、签名信息以及 ZIP文件信息这几个“大头”依然没有任何改善. 那为什么Studio不把这些资源真正删掉呢?事实上Android也知道有这个问题,在它的核心实现ResourceUsageAnalyzer中的 注释也写得非常清楚,并尝试解决这个问题提供了思路.

<上图翻译>我们是否支持运行aapt两次,以重新生成resources.arsc文件 这样我们也可以剥离有价值的资源.我们还没有这样做,因为 ShrinkResources任务中详细说明的原因 我们有两种选择: (1)将资源文件复制到新的目标目录,过滤掉 删除文件资源并通过剥离重写有价值的资源文件 移除的值资源的声明。然后我们在这上面重新运行aapt 新的目标目录。
详细使用参考压缩、混淆和优化您的应用
- 第三阶段:去除真实无用资源资源
那怎么样才能真正实现无用资源的删除功能呢?ResourceUsageAnalyzer的注释中就提供了一个思路,我们可以利用 resources.arsc中Public ID的机制,实现非连续的资源ID. 简单来说,就是keep住保留资源的ID,保证已经编译完的代码可以正常找到对应的资源.

代码优化
1.混淆
今天ProGuard已经是安卓中很常用的工具了,ProGuard的核心优化主要有三个:Shrink、Optimize和Obfuscate,也就是裁剪、优化和混淆.但是十个ProGuard配置九个有坑,特别是各种第三方SDK。我们需要仔细检查最终合并的ProGuard配置文件,是不是存在过度 keep的现象. 你可以通过下面的方法输出ProGuard的最终配置,尤其需要注意各种的keep *,很多情况下我们只需要keep其中的某个包、 某个方法,或者是类名就可以了
-printconfiguration configuration.txt
详细使用参考ProGuard 在 Android 上的使用姿势
除了ProGuard国内鹅厂也开源了一个神器工具AndResGuard,它主要有两个功能,一个是资源 混淆,一个是资源的极限压缩.
先说资源混淆,ProGuard的核心优化主要有三个:Shrink、Optimize和Obfuscate,也就是裁剪、优化和混淆.AndResGuard也有类似的实现, 资源混淆的思路其实非常简单,就是把资源和文件的名字混淆成短路径:
Proguard -> Resource Proguard
R.string.name -> R.string.a
res/drawable/icon -> res/s/a
这样实现对资源文件有优化作用:
- resources.arsc: 因为资源索引文件resources.arsc需要记录资源文件的名称与路径,使用混淆后的短路径res/s/a,可以减少整个文件的大小.
- metadata签名文件: 签名文件MF与SF都需要记录所有文件的路径以及它们的哈希值,使用短路径可以减少这两个文件的大小.
- ZIP文件索引: ZIP文件格式里面也需要记录每个文件Entry的路径、压缩算法、CRC、文件大小等信息。使用短路径,本身 就可以减少记录文件路径的字符串大小.
然后是压缩,AndResGuard的另外一个优化就是极限压缩,它的极限压缩功能体现在两个方面:
- 更高的压缩率: 虽然我们使用的还是Zip算法,但是利用了7-Zip的大字典优化,APK的整体压缩率可以提升3%左右.
- 压缩更多的文件: Android编译过程中,下面这些格式的文件会指定不压缩;在AndResGuard中,我们支持针对 resources.arsc、PNG、JPG以及GIF等文件的强制压缩.
详细使用参考APP瘦身大法
2.去掉Debug信息或者去掉行号
某个应用通过相同的ProGuard规则生成一个Debug包和Release包,其中Debug包的大小是4MB,Release包只有3.5MB。 既然它们ProGuard的混淆与优化的规则是一样的,那它们之间的差异在哪里呢?那就是DebugItem。

DebugItem里面主要包含两种信息: 调试的信息.函数的参数变量和所有的局部变量. 排查问题的信息。所有的指令集行号和源文件行号的对应关系. 事实上,在ProGuard配置中一般我们也会通过下面的方式保留行号信息.
-keepattributes SourceFile, LineNumberTable
对于去除debuginfo以及行号信息更详细的分析,推荐你认真看一下支付宝的一篇文章《Android包大小极致压缩》。通过这个 方法,我们可以实现既保留行号,但是又可以减少大约5%的Dex体积.
3.类重排
当我们在Android Studio查看一个APK的时候,不知你是否知道下图中“defines 19272 methods”和“references 40229 methods”的区别.



这时候我们可以使用Redex工具对dex文件进行类重排将有调用关系的类和方法分配到同一个Dex中,即减少跨Dex的调用的情况.
当然我们也可以 通过打印ClassLoader的类加载信息,获取到加载顺序然后用dx.bat自己重新生成dex文件. 但是由于类的调用关系非常复杂,我们不太可能可以计算出最优解,只能得到局部的最优解。建议还是使用Redex,ReDex在分析类的调用关系后,使用的 是贪心算法计算局部最优值,具体算法可查看CrossDexDefMinimizer。
除了类重排外还Redex还有不少优化,详细了解可以参考基于 Facebook Redex 实现 Android APK 的压缩和优化
4.Dex压缩
我曾经反编译Facebook的APP,发现它竟然只有700多KB的dex文件,要知道谷歌play可是不允许动态下发代码的,那它究竟把代码放到哪里呢?


- 首次启动时候解压时间长: 应用首次启动的时候,需要将secondary.dex.jar.xzs解压缩,根据上图的配置信息,应该一共有11个Dex。 Facebook使用多线程解压的方式,这个耗时在高端机是几百毫秒左右,在低端机可能需要3~5秒。这里为什么不采用 Zstandard或者Brotli呢?主要是压缩率与解压速度的权衡。
- ODEX文件生成时间长: 前面我就讲过,当Dex非常多的时候会增加应用的安装时间。对于Facebook的这个做法,首次生成ODEX 的时间可能就会达到分钟级别。Facebook为了解决这个问题,使用了ReDex另外一个超级硬核的方法,那就是oatmeal。
oatmeal的原理非常简单,就是根据ODEX文件的格式,自己生成一个ODEX文件。它生成的结果跟解释执行的ODEX一样,内部是没有机器码的.

5. Library优化
Library压缩: 跟Dex的压缩一样,Library优化最有效果的方法也是使用XZ或者7-Zip压缩。在默认的lib目录,我们只加载少数启动过程相关的Library,其他的Library就在首次启动时再解压。对于Library格式来 说,压缩率同样可以比Zip高30%左右,效果相当理想. Facebook有一个So加载的开源库SoLoader,它可以跟这套方案配合使用。和Dex压缩一样,压缩方案的主要缺点在于首次启 动的时间,毕竟对于低端机来说,多线程的意义并不大,因此我们要在包体积和用户体验之间做好平衡.
对于Native Library,Facebook中的编译构建工具Buck也有两个比较硬核的高科技。当然在官方文档中是完全找不到的,它们 都隐藏在源码.
- Library合并: 在Android 4.3之前,进程加载的Library数量是有限制的。在编译过程,我们可以自动将部分Library合并成一个。具体思路可以参考《Android native library merging》
- Library裁剪: Buck里面有一个relinker的功能,原理就是分析代码中JNI方法以及不同Library的方法调用,找到没有无用的 导出symbol,将它们删掉。这样linker在编译的时候也会把对应的无用代码同时删掉,这个方法相当于实现了Library的 ProGuard Shrinking功能.

总结
整理此文的时候,我感觉Android系统本身也存在不完善的地方,也正因为有这些不完善的地方,才有了现在百花齐放的开源方案.只有敢于突然系统限制,往往有意想不到到收获,同时我深感国内外大厂的优化手段层出不穷.
参考资料