如果你对App优化比较敏感,那么Apk安装包的大小就一定不会忽视。关于瘦身的原因,大概有以下几个方面:
下表为Apk目录及文件说明:
文件/目录 | 说明 |
---|---|
assets/ | 存放一些静态文件,可以通过AssertManager访问 |
lib/ | 如果该目录存在,一般存放的是NDK编译出来的so |
META-INF/ | 保存着APK的签名信息 |
res/ | 资源文件所在目录,包含drawable、layout等 |
AndroidManifest.xml | 程序全局配置文件 |
classes.dex | Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式 |
resources.arsc | 编译后生成的二进制资源文件 |
做App瘦身之前需要对自己App现有组成有一个清晰的认识,上述解压的方式只能粗略的看出具体目录的大小,但是有用信息仍然有限。
Android Studio 2.2之后有一个功能Analyze APK,方便简单,功能还是Google自带的靠谱;
ClassShark 是一款查看Android执行文件(apk)的浏览工具,可以很方便的打开APK/Class/Jar/res等文件和分析里面的内容。
NimbleDroid 是美国哥伦比亚大学的博士创业团队研发出来的分析Android app性能指标的系统,分析的方式有静态和动态两种方式,其中静态分析可以分析出APK安装包中大文件排行榜,各种知名SDK的大小以及占代码整体的比例,各种类型文件的大小以及占排行,各种知名SDK的方法数以及占所有dex中方法数的比例。
总结:这三种方式都可以对Apk的组成有一个更加清晰的认识,但更加推荐使用AndroidStudio自带的Analyze APK,简单、高效。
使用Analyze APK查看到文件大小之后发现,classes.dex、res、assets、lib等文件较大,哪里的脂肪多,我们就去抽哪里。确定优化方向:
随着版本的迭代,部分功能可能已被去掉,但是其代码还存在项目中。移除无用代码以及无用功能,有助于减少代码量,直接体现就是Dex的体积会变小。
备注:根据经验,不用的代码在项目中存在属于一个普遍现象,相当于僵尸代码,而且这类代码过多也会导致Dex文件过大。
备注:根据经验,项目中存在之前使用之后不使用的库的情况并不罕见。
代码混淆也称为花指令,是将计算机程序的代码转换为功能上等价但是难以阅读、理解的行为。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,虽然效果很明显,但仍然需要谨慎;
一般情况下缩减方法数,都是为了Android著名的64k方法数问题,此处不再回顾,参见之前《关于Multidex的系列文章》。而这里说缩减方法数的目的,是为了App瘦身。
通过《Dalvik Executable format》,我们可以看到Dex文件的组成。而从header-item表中的method-ids-size字段可以看出,方法数缩减之后,可以减少方法列表的大小;同时,方法在Dex文件中的占用空间也减少了,App自然被瘦身。
而缩减方法数,除了上面写到的普遍方法:移除无用方法、库、使用较小的SDK之外还有:
对于重要性,代码和资源的瘦身同样重要,但是从效果上来说,资源文件的瘦身效果比代码的瘦身效果要好非常多。很有可能费力许久在代码上得到的瘦身效果,在资源文件瘦身中轻松就得到了。
移除无用资源文件要比移除无用代码容易,在Android Studio的任何文件中右击,选择清除无用资源即可删除没有用到的资源文件。
备注:在build.gradle中设置shrinkResources为true后,每次打包的时候就会自动排除无用的资源。shrinkResources需要配合minifyEnabled一起使用。但是根据我的实验:无用的资源还是会被打进Apk中,只是变成一张黑图,体积也非常小,只有不到100b。有使用错误的地方欢迎指正!
这条开发者中讨论的比较多,确实Google强烈建议根据不同屏幕密度准备多套切图资源来做适配的。但是鉴于Android上对UI要求不会是最顶级的那种高度,以及即便是放在合适(注意这两个字)一个的目录下,在不同的分辨率下也会做自动的适配(等比例拉伸、缩放);因此还是建议:对UI不是最顶级要求的话根据自己的用户群体机型放在一个合适的目录下。这样毋庸置疑可以缩减Res的大小,进而减少Apk的体积。
备注:图片放在不恰当的目录有可能会对内存产生较大的影响,可以参考之前的文章《Android 性能优化(五)之细说 Bitmap》。
之前我在项目里发现过文件大小过1M的图片,可能是由于UI同学和RD同学的双重疏忽,导致如此大的图片到了项目中,对Apk体积的影响自然不言而喻。
可以考虑使用TinyPng、pngquant、ImageOptim等工具对图片进行压缩,这些工具可以减少PNG文件大小,同时保持图像质量。
此处以TinyPng为例:TinyPng是一个相当不错的图片压缩工具,在保持alpha通道的情况下对PNG的压缩可以达到1/3之内,而且用肉眼基本上分辨不出压缩的损失。这张3.4M的图片被压缩到了984.7k,压缩率高达71%。
也有同学开发了一个AndroidStudio插件:TinyPngPlugin,能够批量地压缩项目中的图片,更加方便。
备注:需要注意的是在Android构建流程中AAPT会使用内置的压缩算法来优化res/drawable/目录下的PNG图片,但也可能会导致本来已经优化过的图片体积变大,可以通过在build.gradle中设置cruncherEnabled来禁止AAPT采用默认方式优化我们已经优化过的图片。
aaptOptions {
cruncherEnabled = false
}
PNG是一种无损格式,JPG是有损格式。JPG在处理颜色很多的图片时,根据压缩率的不同,有时会去掉一些肉眼识别差距较小的中间颜色。但是PNG对于无损这个基本要求,会严格保留所有的色彩数。所以图片尺寸大,或者色彩数量多特别是渐变色的多的时候,PNG的体积会明显大于JPG。
在这种情况下,我们可以有所取舍。小尺寸、色彩较少或者有alpha通道透明度的时候,使用PNG;大尺寸、色彩渐变多的使用JPG。
备注:根据经验,对于可以直接使用JPG格式的图片,最好不要从PNG转换为JPG,而是出图的时候直接出JPG格式的图片,相对而言,后者的效果更好。
可缩放矢量图形(英语:Scalable Vector Graphics,SVG)是一种基于可扩展标记语言(XML),用于描述二维矢量图形的图形格式。SVG由W3C制定,是一个开放标准。可以使用矢量图形来创建独立于分辨率的图标和其他可伸缩图片。使用矢量图片能够有效的减少App中图片所占用的大小,矢量图形在Android中表示为VectorDrawable对象。
优点
缺点
Google于2010年提出了一种新的图片压缩格式 — WebP,为图片提供了无损和有损压缩能力,同时在有损条件下支持透明通道。据官方实验显示:无损WebP相比PNG减少26%大小;有损WebP在相同的SSIM(Structural Similarity Index,结构相似性)下相比JPEG减少25%~34%的大小;有损WebP也支持透明通道,大小通常约为对应PNG的1/3。同时,谷歌于2014年提出了动态WebP,拓展WebP使其支持动图能力。动态WebP相比GIF支持更丰富的色彩,并且也占用更小空间,更适应移动网络的动图播放。
优点:
缺点:
在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的原因:
这里推荐微信的资源混淆方案:AndResGuard。
将部分使用频率不高的资源例如图片,放在网上,在恰当的时机提前下载,这样也能节约部分空间。
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的体积,也不影响性能敏感模块的执行。
移除特定平台So的方式,这样打包就只保存armeabi里的So。
ndk {
//设置支持的SO库架构
abiFilters 'armeabi'
}
备注:原本x86架构的CPU是不支持运行arm架构的So,但Intel和Google合作在x86机子的系统内核层之上加入了一个名为houdini的Binary Translator(二进制转换中间层),这个中间层会在运行期间动态的读取arm指令并将之转换为x86指令去执行。
我们知道Apk文件实际上就是一个Zip文件。Android SDK的打包工具apkbuilder采用的是Deflate算法将Android App的代码、资源等文件进行压缩,压缩成Zip格式,然后签名发布。
既然是压缩,那能不能改进其压缩方式,获取更小的Apk文件?通过分析Apk打包的流程图我们可以发现SignedJarBuilder类对整个工程包括代码Dex和一些课压缩的资源、文件进行压缩,使用的是JDK中zip包下提供的算法。
简单的方式我们可以在不改变App编译器工作的情况下,对生成的Apk文件进行二次压缩,同样使用Deflate算法,但是将压缩等级从标准提升到极限压缩。提高压缩级别可在不对Apk包本身的内容做任何修改的情况下得到更小的Apk。
备注:
使用7Zip对Apk进行极限压缩。
参考: