前言
包体积优化的文章很多,也有很多非常前沿的技术,很多产品都非常重视包体积,因为包体积不仅仅影响的是性能(主要是磁盘和内存I/O),而且还影响新装用户量。

关于包体积的优化,主要有以下手段:
- 删除
- 压缩
- 动态化
- 矢量图
- 自绘制
包体积优化的手段包括常规手段,其中常规手段是最容易实现且收益最明显的手段,非常规手段是优化长尾数据非常有利的工具。
目前而言,各种工具都集中非常规手段方面的优化,其实,在非常规手段中,也有很多简单易用的手段,本篇会进行一些补充。
常规手段
在开始本篇之前,我们来梳理一下目前常用的手段。
删除无用资源
删除无用资源是获得无副作用净收益的最有效的手段,主要有以下几点
- shrinkResources=true可以在打包时删除无用的代码和资源,此方法需要和minifyEnabled=true一起使用
- Lint或者Android Studio中的 Code Clean、Unused resources查找,或者实现python脚本查找未使用的资源,提前手动删除资源
- 删除帧动画中重复的图片,帧动画时间间隔不小于32ms(32ms为2个vsync信号,人眼每秒只能接受24帧,那就是每41ms一张图片,但是3*16=48ms显然不满足,只能在32ms)
- 简单纯色图片删除,尽量使用代码实现
- 只保留有限的so库的arm版本
- 保留有限的语言资源
资源压缩
压缩分为有损压缩和无损压缩,显然,有损压缩能取得最大化的收益,因为不仅仅能优化包体积,还能减少i/O数据量和内存占用,但是有损压缩有一定的副作用,那就是容易出现图片质量下降,不过出现这种问题的概率也是比较低的,如果视觉走查阶段能有效处理的话。
有损压缩
- 有损压缩一般分为质量压缩和尺寸压缩,首先常用手段有
- 图片转码:JPEG(包含很多信息,而且色彩更加丰富)、PNG转为WebP、AVIF格式等,目前主流方式WebP占比较多,同时AVIF优势也很明显,后者受到很多大厂青睐。
- 图片缩小:这种属于尺寸压缩,一般来说是转为缩小后转为.9.png图片,另外就是将图片全部转移至res/mipmap文件下,后者相比res/drawable的优势是在加载图片时会进行线性过滤,减少锯齿和抖动。
- TinyPng、pngQuant压缩等工具压缩,下面是常用工具的对比

- 代码混淆:代码混淆是一种收益非常明显的dex压缩方式,混淆dex压缩最有效的手段。
无损压缩
简单来说,无损压缩是一种减小磁盘空间占用和减少内存i/o的压缩方式,但是无法减少内存占用。常见的无损压缩工具有。
- Bitmap#compress 无损压缩,Bitmap可以让图片质量更低,磁盘空间更小、i/o耗时更少的编码压缩,但是内存占用压缩前后一致。
- zip 压缩:这是一种常见的压缩方式,比较适合CDN方式的,Android中如果想要接入的话,需要使用BitmapFactory#decodeStream + ZipInputStream进行解码
动态化
动态化本质是将资源部署在服务器上,动态下发,常用的手段有
- CDN部署资源 + Glide加载
- 图片资源插件化 + AssetManager加载
- so插件化 + ClassLoader 加载
矢量化
矢量化也是一种非常有效的手段,常用的矢量化手段有SVGA、字体图标等,矢量化可以实现放大缩小不失真,但是缺点是比较受限于纯色图片。
自绘制
计算机中,算法和空间是一对兄弟,当然并不是此消彼长,但是在很多情况下,通过自绘制可以有效的减少包体积,同时还具备矢量性质,在一些情况下,自绘制还可以用于减少布局层级(层级少可以减少xml的体积),也属于收益很大优化方式,不过这点取决于开发者自身水平,当然,常用的手段有如下方式。
- 自定义drawable.xml文件,相比图片,自定义的xml有很多优势,可以有效减少纯色效果的资源使用,如Layer-List组合多种图片,selector实现状态变化等。
- 自定义drawable,Android系统中内置了一些Drawable,我们还可以自定义各种Drawable,如MoonDrawable(每月1-30日的月亮形状),至于自定义的Drawable如何加载,可以参考之前的文章《Android 利用换肤技术,实现自定义Drawable从Xml中加载》
- 自定义View,一些复杂的动画效果、帧动画效果实际上可以通过自定义View实现。同样,对于一些卡片,我们完全可以使用自定义View实现,不仅仅能减少布局层次,还能减少资源的使用。
- 硬编码PaintDrawable,相比于drawable.xml 无法动态设置倒角的问题,可尝试使用PanitDrawable解决
- 硬编码ColorStateList、StateListDrawable,对于复用性不强但是效果变化较多的drawable,可以使用硬编码方式
- 使用Tint Color:Android提供了对于纯色图片的优化手段,因此,对于纯色图片,如需变色可以通过Tint Color实现。
以上都是常规优化手段,下面我们来看看非常高的优化手段。
非常规优化
非常规优化手段有一定的技术要求。
使用 AndResGuard
使用AndResGuard可以有效缩短资源名称和目录名称,从而减少resources.arc和res目录的体积。
facebook redex
redex是基于字节码的优化,属于javac的增强手段,如内联、无用变量删除等,可以一定程度上减少包体积。
减少String相关代码段
代码中不可避免的会有String,如日志的输出,实际上,dex中String的占比非常多,因此,解决这些办法最有效的手段还是减少日志输出。
基本类型常量化
对于基本类型变量和字面量字符串,声明为final类型可以减少iget等操作。
kotlin方法内联优化
内联的正作用是减少方法寻址和跳转,但是缺点是会引发包体积增大。实际上,对于方法体内是单行代码的情况,内联是非常必要的,但是多行代码,就要考虑是否值得。
反-反混淆
在Android,你最常用的混淆规则是配置Serializable相关了,如果不配置可能引发Gson、持久化功能异常
java
-keepnames class * implements java.io.Serializable
但是,Serializable范围可能比较大,一些没必要保留的类无法彻底混淆,如google-guava的一些数据结构,建议自定义规则,而不是笼统的使用Serializable
java
public interface KeepGuard {
}
然后使用下面方式
java
-keepnames class * implements a.b.KeepGuard
当然,如果是老项目,自定义KeepGuard 可能还有风险,那就使用,相当于通过黑名单方式反-反Serializable混淆
java
-applymapping op_mapping.txt
最小化引用so库
对于一些简单的功能,如图片处理、动效等,如果只占引用库能力的1/3甚至更小,这个时候就要考虑其他手段和裁剪第三方so库,比如引入open cv、libjpeg等是否合适。
【死入口】代码删除
前面讲过shrinkResources可以删除无效引用资源和class,但缺陷是无法对【死入口代码删除】
很多开发者在下架某些功能之后,在xml或者直接用setVisiblity(View.GONE)隐藏了入口,或者是使用boolean变量写死了跳转逻辑,但是整体的代码引用链保持存在,这就无法导致入口后面的资源占用被删除。
这里建议对于下架的功能,要remove而不是hide,如果担心后续重新上架,那就是后续版本的问题,不可保留。你可以不相信自己,但你得相信Git。
字体裁剪
前面说过,引入字体图标可以有效减少包体积,然而,有意思的是,一些设计为了实现特殊字体,会让你引入好几兆的字体,但你仅仅需要的可能就那么几个字,这个时候你得找一款字体裁剪工具,删掉无用的字体。
ASM指令优化
除了facebook的redex,开源的byteX或者booster都可以做到一些字节码优化。
同时如果在代码实现中,减少变量的定义或者使用位运算也是减少指令有效的方式。
转视频编码
这种方式也是一种方法,通常意义上,将图片编码为小于0.5s的单帧视频,能获得更小的压缩比率,但是缺点是比较依赖MediaMetadataRetriever的解码速度,另一个限制点是必须符合【视频的宽高比】,同时建议不要大于720p,防止一些设备无法解码大于720p以上的资源。
减少面向未来的编程
一些开发者为了提前布局后续的需求,加入了很多后续版本才需要的资源或者库,这是不明智的,正确的做法是提高可扩展性,面向未来的编程要尽可能避免。
减少重复资源
通过md5、crc32、pHash(相似度为0)检索出重复资源,进行删除。当然这里给一个技巧,重复资源检索有很多种方式,如果追求速度的话那就使用size比较,两者相等的"一般"都重复,而且md5相同的资源,size是一般一样的。
我们可以利用下面结构进行检索,记录结果哦,再进行去重。
java
Map<Long,List<String>>
但是size一样的图片,md5一定一样么?其实对于旋转后的图片,其md5是不一样的,而pHash却能检测出其一样,因此,对于使用md5或者crc32或许并不是最优的,因此,去重务必要注意"漏网之鱼"。
当然,这里建议使用python脚本,而不是定义asm插件,因为毕竟这种优化的频次不是很高。
减少相似资源
这里主要是指相同dpi中相似度较高的资源,在之前一篇文章中,我们提到过相似距离的pHash算法,请参考《Android Bitmap亮度调节、灰度化、二值化、相似距离实现》,不过这个比较依赖android平台,如果要优化包体积建议使用python实现。
相比md5,pHash还能检测出旋转Nx90度(N 为1,2,3)的重复图片,其次还能有效检测出相似图片。
我们的优化点是:
- 删除被旋转后的图片,然后通过代码旋转
- 删除差异不太大的图片
- 删除仅仅存在透明度差异的图片,使用代码实现透明度
总结
APK包体积优化其实很成熟了,往往都是常规手段为主、非常规手段为辅,在这些过程中,往往大家都都能想到这些手段。对于非常规手段,也有些技术含量不太高,但容易忽略的手段,本篇对此进行了补充:
- 死代码入口删除
- 重复图片剔除
- 相似图片
- 字体裁剪
- 反-反Serializable混淆
- 最小化引入so资源
本篇就到这里,希望本篇对你有所帮助。