【性能优化】如何让APK瘦成一道闪电

转载请标注来源:http://www.cnblogs.com/charles04/p/8547273.html


 如何让APK瘦成一道闪电

0、目录

  1. 背景介绍
  2. 分析与探索
  3. 总结与反思
  4. 参考文献

1、背景介绍

随着业务的不断迭代,项目中的APP会不断引入新的技术框架,第三方SDK,资源文件,业务逻辑等,导致APK包的体积不断增大。最近正在研发的一个APP,短短一个月的时间,当前版本相比上一个上线大版本,当前版本的APK体积已经从18M增大到34.6M。

APK体积过大,从编程规范角度讲,影响APK的预制(APK 占据预制存储空间尺寸有限制);从2C的角度讲,APK体积的增大,将增大对用户流量的消耗,延长下载和安装的时间,影响APP下载和安装的成功率,导致用户体验下降,影响用户的留存率。

所以,科学的APK瘦身技术是一件非常有意义的事情。

2、分析与探索

2.1. 系统分析

在进行实际的APK瘦身操作之前,首先对APK包的组成进行分析,从而找准切入点,宏观把控,有的放矢。

通过Android Studio自带的工具分析,当前版本和上一个上线版本的APK组成结构如下所示:

图1. 当前版本的APP的主程结构

图2. 上一个上线版本的APP的主程结构

其中上图中相关标签的含义如下:

Raw File Size:原文件大小,对应APK占物理硬盘的容量(也即通常说到的,apk大小);

Download Size:经过Google Play处理压缩后的apk大小。

从上图中,可以发现

Apk内部主要由res,class.dex,lib,assets等文件组成,其中res,class.dex的占比最大,二者加起来占比在90%以上。

进一步对比当前版本和上一个上线版本,并对两者取差,得到结果如下:

可以发现,对比上一个上线版本,当前版本增加最明显的为res,classes.dex,resource.arsc,这几部分尺寸增加分别如下:

文件名称 尺寸增加(M)
res 14.9
classes.dex 3.5
resource.arsc 1.7

 

 

 

 

 

了解这些数据之后,接下来要对APK包内的每项组成进行进一步分析,根据每项在Android工程中的对应关系,进行针对性瘦身。具体如下:

Res Res: 存放资源文件。包括图片(drawable)、raw文件夹下面的音频文件、各种xml(layout,string,array等)文件等等。
resources.arsc 编译后的二进制资源索引文件
AndroidManifest.xml Android项目的清单文件,它描述了应用的名字、版本、权限、引用的库文件,注册的四大组件等等信息
classes.dex java源码编译后生成的java字节码文件,其中java源码包括本地编写的代码,和SDK中包含的java代码。在Android项目中,每超过65535个方法数,就会新增加一个classes.dex文件,所以,在当前项目中,classes.dex文件有多个
META-INF 存放的是签名摘要信息,用来保证apk包的完整性和系统的安全
lib 存放的是so文件,用于本地混合调用c/c++代码库,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips
assets 存放一些配置文件(比如webview本地资源、图片资源等等),这些文件的内容在程序运行过程中可以使用AssetManager来获取

 

根据APK包中每项的实际存放内容,针对性的设计APK瘦身策略如下:

2.2. Res瘦身

Res文件的瘦身主要涉及资源文件的瘦身。主要从以下几个方面来考虑:

2.2.1. 冗余资源的删除

项目中的图片资源一般是非常多的,人肉搜索和删除费时费力;好在可以通过工具或配置来自动化完成相关工作,我自己比较喜欢用两种方法:

  • Lint

冗余资源可以通过Android Studio的Lint工具来扫描发现,扫描出来之后再批量删除。但是Lint扫描似乎无法扫描出SDK中的资源引用,所以有可能会导致误删的情况,所以删除之后,一定要重新编译,通过之后再提交相关的代码。

  • shrinkResources

gradle配置中将shrinkResources设置为true可以在打包的时候不打包使用不到的资源,shrinkResources是在buildTypes内配置的,具体如下:

1 android {
2 3     buildTypes {
4         release {
5 6              shrinkResources true
7         }
8     } 
9 }

设置之后会延长编译打包的时间,所以不建议在debug环境下使用。

另外,也有一些情况反映将shrinkResources设置为true后有些图片无法正常显示,这个我倒没有遇到过,等我遇到了再回来补个分析。

2.2.2. 资源复用

对于类似的图片,不需要放置多张不同的图片资源,而只需要通过同一张图片,在不同的透明度,缩放比例,旋转等方式来实现复用。这样可以在一定程度上减少需要的图片资源的使用,达到瘦身的目的。

2.2.3. 分辨率瘦身

这个名字是我取的。所以这里想要稍微介绍下,什么叫分辨率瘦身。

在Android开发的过程中,同一张图片一般会适配不同的分辨率(DPI),也即在不同分辨率文件夹中存放不同尺寸的图片资源。实际上,只保留一种source分辨率下的图片,在不同target分辨率的设备上,相比适配不同的source分辨率,二者:

  • 不会存在显示上的差异;
  • 图片本身的内存资源占用也是一致的。

只是在分辨率不匹配的时候,会额外增加一定的图片换算的资源消耗。这里,之前写过两篇相关的博客,有兴趣的可以参考下:

  • 内存占用分析:http://www.cnblogs.com/charles04/p/6804422.html
  • 内容显示分析:http://www.cnblogs.com/charles04/p/6914859.html

考虑到额外的内存等资源的消耗,分辨率瘦身还是要慎重使用,但是在一些特殊的场合,分辨率瘦身兼职是量身定做的。比如说EMUI预制,也即APP预装到指定的手机,手机的Target分辨率是固定的,所以只需要适配一套Source分辨率就可以啦。

关于分辨率瘦身的具体实现,最简单的是将其他分辨率资源的图片全部删掉。但是这样改动比较大,而且如果在有些疏忽的情况,没有在指定的分辨率上添加资源,也有可能会造成ResourceNotFound的错误,风险比较大。

实际上,可以通过Gradle的分包策略来简单实现分辨率瘦身。

既然说到这里,那就简单介绍下Gradle的分包策略。Gradle分包是指通过Gradle中的配置,实现自定义的资源或逻辑打包,目前用途比较广泛的主要包含两方面的内容:

(1) 多渠道打包

多渠道打包是通过Flavor来实现,可以实现资源文件,方法类,so,甚至是Manifest等配置文件的编译隔离。这里先简单说一嘴,按下不细表,后面会专门给这个开专题。

//todo:专题传输门

(2) splits编译时资源分包

顾名思义,这种分包策略就是在编译的时候,根据编译的资源文件的不同,分别打出不同的APK包。本文提到的分辨率瘦身就是通过这种分包方式来实现的。

这里简单介绍下splits分包中Gradle配置的具体使用,如下:

  • splits:关键词,表示当前是分包的配置啦
  • density:具体分包的内容,当前支持density和abi,其中density就表示资源的分辨率,abi表示的是so库的架构类型,敲黑板,abi后面会用到,这个先不细说了;
  • enable:使能,true表示当前的配置生效,false表示不生效;
  • reset():初始化,调用后,当前的gradle相关配置会清空,一般要搭配后续的include和exclude使用;
  • include:想要使用的类型(分辨率/架构子类,下同);
  • exclude:在整体集合中想要去除的类型;
  • universalApk:是否要同时生成一个包含全部类型的APK,true表示是,false表示不是;

所以,如果只保留xx分辨率,具体分包配置如下:

 1 android {
 2   ...
 3   splits {
 4     density{
 5       enable true
 6       reset()
 7       include "xxhdpi"
 8       universalApk false
 9     }
10   }
11 }

2.2.4. 资源压缩

目前Android中主流的图片格式为png和.9,在某些对透明度不做要求的场景中也会使用jpg格式。但是实际上,png和jpg对图片的压缩都很难再尺寸和质量上取得双向突破。

然而,google推出webp压缩之后,一切将被改变。Webp可以在大幅降低图片体积的情况下,保障图片的质量(肉眼几乎感受不到图片质量的下降),关于png和webp压缩的比较,有如下示例:

经过实践,在APK瘦身中,推荐使用65%-80%的有损webp压缩。

Webp压缩可以通过专门的工具来实现,也可以通过如下的在线转换工具。

1 http://zhitu.isux.us/

2.3. Dex文件瘦身

2.3.1. 概况分析

在分析class.dex瘦身之前,首先介绍下dex文件时如何生成的。Dex文件是Android虚拟机上的可执行文件,在工程中是从Java-Class-Dex的生成过程,具体如下:

  1. Java文件:Java文件是在工程中通过Java高级语言编写出来的代码逻辑文件,这是离软件开发人员最近的文件;
  2. Class文件:Class文件时通过编译器编译生成的目标文件;
  3. Dex文件:Class文件只是编译过程的中间目标文件,不可以被虚拟机运行,而Dex文件正是通过Class文件生成的Android虚拟机上的可执行文件,虚拟机通过ClassLoader可以直接加载Dex文件,为用户展示代码效果。具体ClassLoader加载过程是一套很有挖掘价值的技术体系,有机会会做一些相关的总结。

另外,通过APK解压我们会发现,APK中的Dex文件可能会有多个,这里又涉及到另外一个话题,叫做Dex分包技术,在后续热修复插件化的相关话题中会重点介绍,这里不做赘述。

2.3.2. 具体策略

好了言归正传,知道了Dex文件的生成原理之后,就可以做针对性的瘦身,那就是对工程中的Java代码进行瘦身。

总结来说,Java文件瘦身大概有以下途径:

(1) 冗余代码删除

可以通过Android自带的Lint工具进行冗余代码的检测和删除。不过Lint只能排查出不再调用的代码逻辑,实际上有很多不再使用的代码逻辑还在被各种调用,这些就依赖软件开发人员的敏感度和质量意识啦。

(2) 代码混淆

ProGuard可以对代码进行混淆,优化和压缩,在Android工程中,可以在gradle中通过如下的配置进行混淆:

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

其中proguard-rules.pro表示自定义的ProGuard规则。

(3) SDK优化

Android业界有相当多经典的轮子,有了这些轮子可以更加快捷的借鉴前人的技术结晶,更加快速的构建自己的项目。

但是,为了保障接口的稳定性,功能的完整性,SDK的作者一般会Release出来一些大而全的SDK,实际上,我们可能只需要使用其中很小一部分功能,却要继承一个厚重的SDK,这是很不合理的。

遇到类似的情况时,推荐对SDK进行二次重构。从既有架构中抽离出项目中想要依赖的部分。

在实际项目中,我也有过类似尝试,曾经将华为移动服务的AAR完全拆开,抽离其中的PUSH功能,而将支付,钱包,登录等功能去除掉,重组出一个新的Jar包,既满足了项目的实际需求,又减少了APK Size,完美。

不过,在实际的开发过程中,实现SDK的优化并不简单,这是因为SDK的代码的类和方法大都经历过混淆,可读性较差,要从有限的信息中理解代码的逻辑,并抽离出项目中需要的那部分,是一件技术难度较大的事情,所以可以根据实际情况,相机行事。 

(4) 动态加载

对于有些特定用户才会使用到的,并且代码量较大的功能,可以通过动态加载的方式,展示给用户。

也就是说,在APK打包的时候,不将这部分代码打包到APK,而在用户触发了某些动作,例如安装了某些设备,注册了某个功能的时候,再去服务器动态下载这部分代码,然后动态加载这部分下载的功能。

其实这就是插件化的概念。目前插件化已经发展的相当成熟,业界也有相当多功能和性能都非常优异的插件化解决方案,例如360推出的Replugin,任玉刚的Dynamic-load-apk,林光亮的Small,等等。关于插件化的知识后续也会陆续推出,同样的,这里暂时按下不表(快按不住了)。

2.3.3. 小结

对于代码优化这块,想顺便扯一下。通过观察发现,身边很多开发者有个误区,那就是没有去全面熟悉早期的代码架构,在进行功能迭代的时候,为了不影响之前的功能,特别喜欢在早期的代码上打补丁,导致整体的代码架构臃肿不堪。这样导致的问题是,不仅增加维护的成本,而且APK的体积也会因为class.dex文件的增大而随之增加。

就个人习惯,在接手新的项目或新的模块的时候,会习惯性的对代码进行整体的优化重构,对逻辑进行归纳整合,去除由于方案变更等原因而不再使用的冗余的代码块和代码分支。这样,不仅代码逻辑更加稳健,而且对APK的瘦身也是有所裨益的。

2.4. resource.arsc文件瘦身

resource.arsc是一个索引文件,表征着资源id和资源之间的对应关系。这里的资源文件包括图片资源,xml资源,string资源等各项资源。所以,前面讲到的清楚无效的资源,减少资源引用都会对resource.arsc进行瘦身。

另外,对于字符串资源,如果只用到部分小语种,也可以通过gradle来配置语种的支持,避免多余的资源索引的生成,例如只需要支持中英港台四种语种的时候,可以设置如下:

1 defaultConfig {
2     resConfigs "zh-rCN", "zh-rHK", "zh-rTW", "en"
3 }

2.5. Lib文件瘦身

2.5.1. 概况分析

Lib文件夹中主要保存的是动态加载的so库。对于不同的CPU架构可能需要适配不同的so文件,如下所示:

Android系统目前支持以下七种不同的CPU架构:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起)

每一个CPU架构对应一个ABI:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。对于不同的架构,有如下的兼容性:

  1. x86设备一般对arm类型的函数库支持度良好(部分及其老的设备不考虑);
  2. 位数向下兼容,64位的设备支持32位的函数库,但是会丢失64位函数库上优化过得性能,例如ART,webview,media等;

就目前市场份额而言,绝大多数手机设备都已经是ARM(armeabi-v7a占较大的市场比重)类型的架构了,而很少使用mips和x86类型的架构。有人问为啥(谁问了?明明是你自己想说。。),这里简单说一下,其实很简单,高性能+低功耗。

前面提到的主流的三种CPU架构有如下特点:

  1. ARM:体积小,低功耗,低成本,高性能,这意味着可以在处理复杂的操作,同时保持较高的续航能力,这种CPU架构简直是为手机设备量身定做的;
  2. MIPS:学术化的成果,学院派发展与风格导致在商用上不是很成功,目前在高清盒子,打印机等设备上运用较多,在手机上用的较少;
  3. x86:高性能,高功耗,目前属于PC市场的王者,虽然也在朝着移动端设备发展,但是由于续航等问题,暂时还是处于被ARM架构碾压的状态。

书接上文,言归正传。再讨论回CPU架构指令的兼容问题,MIPS架构虽然不支持运行ARM指令,但是由于主流市场上几乎没有MIPS架构设备,所以可以不作考虑。而x86架构兼容ARM指令,ARM架构对ARM指令集向下兼容,所以实际上,可以只保留armeabi架构的so文件即可。

在具体实现上,可以通过如下方法:

 2.5.2. 具体策略

(1) 直接删除非armeabi的so和相应的文件夹

但是要注意,必须要删除所有的非armeabi文件,因为如果只删除部分非armeabi文件夹下的so文件的话,APK打包的时候还是会生成armeabi-v7a等文件夹,在实际运营过程中,如果设备是armeabi-v7a架构的话,首先会去armeabi-v7a文件夹下寻找对应的so,如果已经存在armeabi-v7a文件夹,就不会去其他兼容性文件夹下面去寻找,这样就有可能会包so资源无法找到的错误。

(2) Gradle中属性配置

可以通过在Gradle中配置ndk属性和splits属性来分别实现so的瘦身,二者实现的效果是类似的。

其中ndk的具体配置如下:

1 defaultConfig {
2     ndk {
3         abiFilter "armeabi"
4     }
5 }

同样可以使用前文提到的分包(splits)的方法(终于接上了…),具体来说,就是通过配置splits中的abi属性来实现,如下:

 1 android {
 2     ...
 3     splits {
 4         abi {
 5             enable true
 6             reset()
 7             include "armeabi"
 8             universalApk false
 9         }
10     }
11 }

除此之外,AndroidManifest.xml清单文件,META-INF文件,assets等在APK中体积占比较小,且可压缩可见不大,一般在APK瘦身中不做重点考虑。

3、总结与反思

(1) APK瘦身涉及到用户切身的体验问题,是APK性能优化领域一件非常有意义的探索;

(2) 在进行一件系统性事务的时候,应该从源头着手,做系统性分析,然后根据分析进行针对性突破,例如,在进行APK瘦身的时候,首先要对APK进行分析,找准努力的方向;

(3) APK瘦身主要的着力点是资源文件的瘦身,dex文件的瘦身,资源索引resource.arsc的瘦身,以及so库的瘦身;

(4) 组件化和插件化与APK瘦身也是息息相关,对于功能比较分散的大型APK,在应用市场放置一个壳子,在用户使用到相关功能的时候,动态下载和加载相关的功能代码,是一个不错的瘦身方案,同时,通过版本管理,还可以对插件功能进行热更新;

(5) 代码质量意识一定要增强;整洁而不冗余的代码和资源,可以极大地防止APK体积快速挣增长。

4、参考文档

(1) https://techblog.toutiao.com/2017/05/16/apk/

(2) https://tech.meituan.com/android-shrink-overall-solution.html

(3) http://www.cnblogs.com/tianzhijiexian/p/4505312.html

(4) https://zhuanlan.zhihu.com/p/21962184

(5) http://mobile.51cto.com/aprogram-493310.htm

(6) https://www.jianshu.com/p/02cb9a0eb2a0

 

原文地址:https://www.cnblogs.com/charles04/p/8547273.html