1、 前言

如果你对App优化比较敏感,那么Apk安装包的大小就一定不会忽视。关于瘦身的原因,大概有以下几个方面:

  • 对于用户来说,在功能差别不大的前提下,更小的Apk大小意味更少的流量消耗,也意味着更多的用户下载;
  • 对于产品来说,大于竞品的Apk意味着较低的下载基数,不利于验证产品策略;
  • 对于开发人员来说,App瘦身则是一次技术优化、技术提升的机会;

2、 Apk的组成

2.1 Apk典型组成




 


性能优化7--App瘦身_android


一个典型的Apk组成


下表为Apk目录及文件说明:

文件/目录

说明

assets/

存放一些静态文件,可以通过AssertManager访问

lib/

如果该目录存在,一般存放的是NDK编译出来的so

META-INF/

保存着APK的签名信息

res/

资源文件所在目录,包含drawable、layout等

AndroidManifest.xml

程序全局配置文件

classes.dex

Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式

resources.arsc

编译后生成的二进制资源文件

2.2 示例分析

做App瘦身之前需要对自己App现有组成有一个清晰的认识,上述解压的方式只能粗略的看出具体目录的大小,但是有用信息仍然有限。

2.2.1 Android Studio Analyze APK

Android Studio 2.2之后有一个功能Analyze APK,方便简单,功能还是Google自带的靠谱;

  • 查看apk中任意文件的大小,得到一个直观的认识;
  • 了解Dex文件的组成,查看使用那些开源库等;
  • 查看二进制文件(如AndroidMainfest.xml等);
  • Apk的比较,便于发现两个版本之间的区别。




 


性能优化7--App瘦身_资源文件_02


Analyze APK的使用


2.2.2 反编译工具ClassyShark

ClassShark 是一款查看Android执行文件(apk)的浏览工具,可以很方便的打开APK/Class/Jar/res等文件和分析里面的内容。




 


性能优化7--App瘦身_so文件_03


ClassyShark的使用


2.2.3 ​​Nimbledroid​

NimbleDroid 是美国哥伦比亚大学的博士创业团队研发出来的分析Android app性能指标的系统,分析的方式有静态和动态两种方式,其中静态分析可以分析出APK安装包中大文件排行榜,各种知名SDK的大小以及占代码整体的比例,各种类型文件的大小以及占排行,各种知名SDK的方法数以及占所有dex中方法数的比例。




 


性能优化7--App瘦身_派生类_04


文件大小排行




 


性能优化7--App瘦身_so文件_05


方法数统计


总结:这三种方式都可以对Apk的组成有一个更加清晰的认识,但更加推荐使用AndroidStudio自带的Analyze APK,简单、高效。

使用Analyze APK查看到文件大小之后发现,classes.dex、res、assets、lib等文件较大,哪里的脂肪多,我们就去抽哪里。确定优化方向:

  1. 代码部分:冗余代码、无用功能、代码混淆、方法数缩减等;
  2. 资源部分:冗余资源、资源混淆、图片处理等;
  3. 对So文件的处理等。

3、 Apk瘦身之代码瘦身

3.1 移除无用代码、功能

随着版本的迭代,部分功能可能已被去掉,但是其代码还存在项目中。移除无用代码以及无用功能,有助于减少代码量,直接体现就是Dex的体积会变小。

备注:根据经验,不用的代码在项目中存在属于一个普遍现象,相当于僵尸代码,而且这类代码过多也会导致Dex文件过大。

3.2 移除无用的库、避免功能雷同的库

3.2.1 项目中基础功能的库要统一实现,避免出现多套网络请求、图片加载器等实现。
3.2.2 不用的库要及时移除出项目,例如我们之前确定要由某推送切换到某推送的时候,此时就要把最初项目中的推送库去掉,而不应该只是注释掉其注册代码。
3.2.3 一些功能可以曲线救国的话就不要引入SDK,例如定位功能,可以不引入定位SDK,而通过拿到经纬度然后调用相关接口来实现;同样实现了功能而没有引入SDK。
3.2.4 而引入SDK也需要考虑其方法数,可以使用ClassyShark、Nimbledroid或者​​APK method count​​等工具查看。

备注:根据经验,项目中存在之前使用之后不使用的库的情况并不罕见。

3.3 启用Proguard

代码混淆也称为花指令,是将计算机程序的代码转换为功能上等价但是难以阅读、理解的行为。Proguard是一个免费的Java类文件压缩、优化、混淆、预先验证的工具,可以检测和移除未使用的类、字段、方法、属性,优化字节码并移除未使用的指令,并将代码中的类、字段、方法的名字改为简短、无意义的名字。

可以看出Proguard不仅能将diamante中的各种元素改的简短,还可以移除冗余代码,因此可以减少Dex文件的大小。

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


其中,proguard-android.txt是获取默认ProGuard设置,proguard-rules.pro文件用于添加自定义ProGuard规则。

备注:对于Proguard,虽然效果很明显,但仍然需要谨慎;

  • 代码混淆会拖慢项目构建速度,因此debug模式下关掉Proguard不至于RD在运行代码的时候抓狂;
  • 因为在debug模式下关掉了Proguard,如果混淆规则没有配置好,在Release模式可能会出现debug模式下不出现的Bug;
  • Proguard也不是你想搞就能搞,如果App做了一段时间之后再来做Proguard,项目包结构不规范的话,那Proguard的规则将会非常多。而短时间内调整包结构也是一件相当痛苦的事情。

3.4 缩减方法数

一般情况下缩减方法数,都是为了Android著名的64k方法数问题,此处不再回顾,参见之前​​《关于Multidex的系列文章》​​。而这里说缩减方法数的目的,是为了App瘦身。

通过《Dalvik Executable format》,我们可以看到Dex文件的组成。而从header-item表中的method-ids-size字段可以看出,方法数缩减之后,可以减少方法列表的大小;同时,方法在Dex文件中的占用空间也减少了,App自然被瘦身。

而缩减方法数,除了上面写到的普遍方法:移除无用方法、库、使用较小的SDK之外还有:

  1. 避免在内部类中访问外部类的私有方法、变量。挡在Java内部类(包含匿名内部类)中访问外部类的私有方法、变量的时候,编译器会生成额外的方法,会增加方法数;
  2. 避免调用派生类中的未被覆写的方法,避免在派生类中调用未覆写的基类的方法;避免用派生类的对象调用派生类中未覆盖的基类的方法。调用派生类中的未被覆盖的方法时,会多产生一个方法数;
  3. 去掉部分类的get、set方法;当然这样会牺牲一些面向对象的观念。

4、 Apk瘦身之资源瘦身

对于重要性,代码和资源的瘦身同样重要,但是从效果上来说,资源文件的瘦身效果比代码的瘦身效果要好非常多。很有可能费力许久在代码上得到的瘦身效果,在资源文件瘦身中轻松就得到了。

4.1 移除无用的资源文件

移除无用资源文件要比移除无用代码容易,在Android Studio的任何文件中右击,选择清除无用资源即可删除没有用到的资源文件。




 


性能优化7--App瘦身_矢量图_06


Remove Unused Resources


备注:在build.gradle中设置shrinkResources为true后,每次打包的时候就会自动排除无用的资源。shrinkResources需要配合minifyEnabled一起使用。但是根据我的实验:无用的资源还是会被打进Apk中,只是变成一张黑图,体积也非常小,只有不到100b。有使用错误的地方欢迎指正!

4.2 Drawable目录只保留一份资源

这条开发者中讨论的比较多,确实Google强烈建议根据不同屏幕密度准备多套切图资源来做适配的。但是鉴于Android上对UI要求不会是最顶级的那种高度,以及即便是放在合适(注意这两个字)一个的目录下,在不同的分辨率下也会做自动的适配(等比例拉伸、缩放);因此还是建议:对UI不是最顶级要求的话根据自己的用户群体机型放在一个合适的目录下。这样毋庸置疑可以缩减Res的大小,进而减少Apk的体积。

备注:图片放在不恰当的目录有可能会对内存产生较大的影响,可以参考之前的文章《Android 性能优化(五)之细说 Bitmap》

4.3 对图片进行压缩

之前我在项目里发现过文件大小过1M的图片,可能是由于UI同学和RD同学的双重疏忽,导致如此大的图片到了项目中,对Apk体积的影响自然不言而喻。

可以考虑使用TinyPng、pngquant、ImageOptim等工具对图片进行压缩,这些工具可以减少PNG文件大小,同时保持图像质量。

此处以TinyPng为例:TinyPng是一个相当不错的图片压缩工具,在保持alpha通道的情况下对PNG的压缩可以达到1/3之内,而且用肉眼基本上分辨不出压缩的损失。这张3.4M的图片被压缩到了984.7k,压缩率高达71%。




 


性能优化7--App瘦身_so文件_07


使用TinyPng压缩图片示例


 

也有同学开发了一个AndroidStudio插件:​​TinyPngPlugin​​,能够批量地压缩项目中的图片,更加方便。

备注:需要注意的是在Android构建流程中AAPT会使用内置的压缩算法来优化res/drawable/目录下的PNG图片,但也可能会导致本来已经优化过的图片体积变大,可以通过在build.gradle中设置cruncherEnabled来禁止AAPT采用默认方式优化我们已经优化过的图片。

aaptOptions {
cruncherEnabled = false
}


4.4 PNG转换JPG

PNG是一种无损格式,JPG是有损格式。JPG在处理颜色很多的图片时,根据压缩率的不同,有时会去掉一些肉眼识别差距较小的中间颜色。但是PNG对于无损这个基本要求,会严格保留所有的色彩数。所以图片尺寸大,或者色彩数量多特别是渐变色的多的时候,PNG的体积会明显大于JPG。

在这种情况下,我们可以有所取舍。小尺寸、色彩较少或者有alpha通道透明度的时候,使用PNG;大尺寸、色彩渐变多的使用JPG。

备注:根据经验,对于可以直接使用JPG格式的图片,最好不要从PNG转换为JPG,而是出图的时候直接出JPG格式的图片,相对而言,后者的效果更好。

4.5 使用矢量图

可缩放矢量图形(英语:Scalable Vector Graphics,SVG)是一种基于可扩展标记语言(XML),用于描述二维矢量图形的图形格式。SVG由W3C制定,是一个开放标准。可以使用矢量图形来创建独立于分辨率的图标和其他可伸缩图片。使用矢量图片能够有效的减少App中图片所占用的大小,矢量图形在Android中表示为VectorDrawable对象。

优点

  • 图片扩展性:不损伤图片质量,一套图适配所有;
  • 图片非常小:比使用位图小十几倍,有利于减小apk体积;

缺点

  • 性能优损失,系统渲染VectorDrawable需要花费更多时间,因为矢量图的初始化加载会比相应的光栅图片消耗更多的CPU周期,但是两者之间的内存消耗和性能接近;
  • 矢量图主要用在色调单一的icon。

4.6 使用WebP

Google于2010年提出了一种新的图片压缩格式 — WebP,为图片提供了无损和有损压缩能力,同时在有损条件下支持透明通道。据官方实验显示:无损WebP相比PNG减少26%大小;有损WebP在相同的SSIM(Structural Similarity Index,结构相似性)下相比JPEG减少25%~34%的大小;有损WebP也支持透明通道,大小通常约为对应PNG的1/3。同时,谷歌于2014年提出了动态WebP,拓展WebP使其支持动图能力。动态WebP相比GIF支持更丰富的色彩,并且也占用更小空间,更适应移动网络的动图播放。

优点:

  • WebP在同画质下体积更小,WebP支持透明度,压缩比比JPEG更高但显示效果却不输于JPEG;
  • 可以通过工具、云服务等进行PNG到WebP的转换;

缺点:

  • Android从4.0才开始WebP的原生支持,意味着要兼容4.0以下机型需要添加适配库;当然现在市面上适配4.0以下的应用已经很少了。
  • Android 4.2.1+才支持显示含透明度的WebP,因此最低版本小于4.2.1的App也不是想用就能用的。可以将不显示透明度的图片转换为WebP。

4.7 资源混淆

在Apk打包过程中,aapt会将每一个资源生成一个对应的int数值,而我们通过这个int值来查找使用资源。在Apk构成中,我们可以看到里面有一个resources.arsc文件,里面保存着资源id和资源key的映射关系。

当调用图片时,先找到drawable分类,再根据当前的系统config找到匹配的config表,根据id找到对应的res数据。drawable在arsc中是当做string类型保存的,res数据中有这个资源在res string pool池中的索引。根据这个索引可以在字符串池中找到一个字符串。这个字符串其实就是一个路径,比如:res/drawable-xhdpi/icon.png;混淆就是将这个路径改为R/s/f.png;同时修改resources.arsc文件的映射关系。这样就能清楚的看出来资源混淆能减小Apk的原因:

  • resources.arsc变小;
  • 文件信息变小,采用了超短路径,res/drawable-xhdpi/icon.png被修改为R/s/f.png。

这里推荐微信的资源混淆方案:​​AndResGuard​​。

4.8 资源在线化

将部分使用频率不高的资源例如图片,放在网上,在恰当的时机提前下载,这样也能节约部分空间。

5、 Apk瘦身之So瘦身

So(shared object,共享库)是机器可以直接运行的二进制代码,是Android上的动态链接库,类似于Windows上的dll。每一个Android应用所支持的ABI是由其APK提供的.so文件决定的,这些so文件被打包在apk文件的lib/目录下。

So的常见的场景如:加解密算法、音视频编解码、核心代码等。在生成SO文件时,需要考虑适配市面上不同手机CPU架构,而生成支持不同平台的SO文件进行兼容。目前Android共支持七种不同类型的CPU架构,分别是:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),Mips (从2012年起),ARMv8,Mips64和x86_64 (从2014年起)。

理论上对应CPU架构的So的执行效率是最高的,但是这样会导致在libs目录下放置各个架构平台的So文件,Apk文件的大小自然也就更大了。那么我们自然想到缩减Libs的目录,一般情况(注意限定)下留下armeabi目录即可,armeabi目录下的So可以兼容别的平台的So,但是性能会有所损耗,失去对特定平台的优化。

因此需要根据自己使用到的So功能来做具体的区分:对于性能敏感模块使用的So可以都放在armeabi目录,然后通过代码判断设备的CPU类型,再加载其对应架构的SO文件,例如微信就是这么做的。既缩减了Apk的体积,也不影响性能敏感模块的执行。




 


性能优化7--App瘦身_矢量图_08


微信So的使用


移除特定平台So的方式,这样打包就只保存armeabi里的So。

ndk {
//设置支持的SO库架构
abiFilters 'armeabi'
}


备注:原本x86架构的CPU是不支持运行arm架构的So,但Intel和Google合作在x86机子的系统内核层之上加入了一个名为houdini的Binary Translator(二进制转换中间层),这个中间层会在运行期间动态的读取arm指令并将之转换为x86指令去执行。

6、 Apk瘦身之7Zip压缩

我们知道Apk文件实际上就是一个Zip文件。Android SDK的打包工具apkbuilder采用的是Deflate算法将Android App的代码、资源等文件进行压缩,压缩成Zip格式,然后签名发布。

既然是压缩,那能不能改进其压缩方式,获取更小的Apk文件?通过分析Apk打包的流程图我们可以发现SignedJarBuilder类对整个工程包括代码Dex和一些课压缩的资源、文件进行压缩,使用的是JDK中zip包下提供的算法。




 


性能优化7--App瘦身_派生类_09


使用7Zip压缩


简单的方式我们可以在不改变App编译器工作的情况下,对生成的Apk文件进行二次压缩,同样使用Deflate算法,但是将压缩等级从标准提升到极限压缩。提高压缩级别可在不对Apk包本身的内容做任何修改的情况下得到更小的Apk。

备注:

  • 需要注意这样极限压缩之后的签名被破坏,需要重新签名。
  • Android平台对Apk安装包的解压算法只支持Deflate算法,其它算法如LZMA,虽然压缩率更好,但是由于Android平台默认不支持,所以如果采用这种算法压缩Apk,会导致Apk无法安装。
  • 目前在Mac上没发现好用的7Zip压缩软件,需要在Windows下使用。

7、 App瘦身总结:

7.1 代码瘦身

  • 移除无用代码、功能;
  • 移除无用的库、避免功能雷同的库;
  • 启用Proguard;
  • 缩减方法数;

7.2 资源瘦身

  • 移除无用的资源文件;
  • Drawable目录只保留一份资源;
  • 对图片进行压缩;
  • PNG转换JPG;
  • 使用矢量图;
  • 使用WebP;
  • 资源混淆;
  • 资源在线化;

7.3 So瘦身

  • 在允许的情况下,针对用户机型分布保留特定架构的So;

7.4 7Zip压缩

使用7Zip对Apk进行极限压缩。

7.5 其它

  • 类如插件化,将Dex与资源文件放在服务端,需要时下载;但是插件化实施以及与现有项目结合难度不小,也超出本文主题,不细说;
  • 通过在 build.gradle配置include来针对每个CPU架构生成单独的安装包,按照架构上传Apk;但是这个方案在国内应用市场几乎没有采用的,只能在Google Play上使用。
  • 一点经验:对Apk进行瘦身,瘦身So以及资源文件是见效最快的操作。瘦身So以及删除不用的图片、压缩图片之后,Apk会缩减很大的比例;而针对Dex的优化可能作用不会很明显。