前言:
Android 开发中,一个成熟的App没有了前期快速迭代的试错,并且业务需求逐渐稳定,功能稳定,必然会面临着包体增大的问题。导致的这样的问题即与前期快速迭代无暇梳理app有关,也与用户增多功能日渐增加有关,而不断增大的包体,每次更新耗费了用户更多的流量,影响到用户的用户体验,就会导致下载量的下降,成为公司的损失,在用户获取成本高的互联网行业,这种影响更为沉重;所以我个人希望减小App包体大小应该是我们App开发者职业目标;
Android Apk包的组成:
一个Apk包的简而言之有8部分组成:
classes.dex
编写的所有的Java代码(包括各种引入的sdk代码)最终转化成在Android虚拟机上运行需要的字节码(和java的字节码有一定的区别)
res文件夹
存放所有资源的文件夹(除了里面raw文件夹的文件不会被编译,其他都会被编译)
resources.arsc
编译后的二进制资源文件
assets文件夹
用于保存需要保持原始文件的资源文件(这部分资源不会被编译)
lib文件夹
用于存放应用需要的native库文件
AndroidManifest.xml
程序全局配置文件
META-INF文件夹
存放几个签名校验相关的文件,用于保证APK的完整性和安全性
其他
其他一些配置生成的文件
分析包体成分占比工具
优化包体之前,我必须要了解到自己项目包体的中各个成分的大小,因为每个部分的优化方案各不相同,只有充分了解包体的情况才能制定有效的压缩方案,才能做到有的放矢。
通常来说分析包体有三种方式:
1、直接使用反编译或压缩工具解压正式APK包,使用操作系统的文件命令排序显示各个文件夹的大小
2、如果android studio是2.1.2之后地版本,可以直接将Apk文件拖进studio中,studio会自动进行分析,列出排序表
3、使用NimbleDroid(点击可跳转)在线工具,此工具是美国哥伦比亚一名教授带领团队开发,功能更全面,更强大。它除了了可以分析包体成分占比,还可以帮我分析第三放SDK的大小,以及方法数的多少,可以帮助我慎重选择第三方库(包体的大小有很大程度上就是选择过多第三方库导致的)
下面是我接手过的一个App没有优化之前的包体结构以及成分占比图:
压缩包体的方法
一、压缩资源文件
1、压缩图片
压缩图片,一般是指对图片进行质量、尺寸压缩和对图片格式进行转换。前者通过在线图片压缩工具或者Linux 的ImgPack批处理脚本完成(现在比较好用的在线图片的工具TinyPing),后者需要根据Jpg、png、gif等图片格式的优缺点进行相应的转换,已达在保证显示效果的情况下减少文件大小,如果对jpg等格式不理解,可以让UI将一张图同事生成三种格式,对比大小和显示效果就可以了。
2、压缩音视频
多媒体文件通常有两种压缩方式,即有损压缩与无损压缩,对于有损压缩网上有一些误解,以为有损压缩的音质画质一定会很差,其实有损压缩只是采用压缩算法是忽略了原始数据中的一些数据,但是都是人的生理极限为前提忽略的。比如视网膜屏做到了每英寸326的像素停止更精细的追求,高清电影的帧率达到60fps就没在进步,这些不是因为技术达不到,而是人眼对分辨像素的极限但是每英寸326ps,对动画频率的识别的极限是60fps,没有必要再去要第327个像素和第61帧。图片压缩就是尽量压缩掉人眼不可识别的冗余数据,既保证显示有照顾到图片大小。
无损格式:WAV,PCM,ALS,ALAC,TAK,FLAC,APE,WavPack ( WV )
有损格式:MP3,AAC,WMA,Ogg Vorbis
实际开发中需要使用音频文件尽量采用 MP3、Ogg 这种有损格式,尽量不要用 WAV、PCM 这种无损音频。
3、在工程中保存一份图片资源
开发目录下会有个drawable或者mipmap目录用于适配不同dpi的屏幕,目前市面上绝大部分机型都处于xxhdpi的适配范围,所以可以考虑只保留xxhdpi目录下一份图片资源,具体保留哪个目录下的资源和保留几份资源还得依照应用自身的实际机型分布决定
此种做法的理论依据可以浏览我收藏这两篇博客:
Android drawable微技巧,你所不知道的drawable的那些细节
4、使用 Drawable XML、Color代替PNG图片
一些情况下,我们可以考虑使用 Drawable XML 来代替 PNG,如:渐变的背景图,用几行 XML 就可以描绘出来,何必使用几十到上百K的 PNG 文件。
用 Color 代替 PNG,如:纯色的背景。
从性能上看,比起使用图片资源需要先将其生成 Bitmap 再传到底层交由 GPU 渲染,用Drawable XML和Color则更加高效,它是直接将 Shape 指令传到底层由 GPU 进行渲染,CPU和内存的占用会更少。
5、不需要透明度时使用JPG代替PNG(这点是对1点的一个补充)
当不需要透明度的图片时,可以考虑用JPG代替PNG,由于JPG没有Alpha 通道,所以文件更小。
6、考虑使用SVG格式图片去替换一些icon
SVG的全称是Scalable Vector Graphics,叫可缩放矢量图形。它和位图(Bitmap)相对,SVG不会像位图一样因为缩放而让图片质量下降。它的优点在于体积小,不用考虑屏幕适配问题。Android 5.0中引入了 VectorDrawable 来支持矢量图(SVG),同时还引入了AnimatedVectorDrawable 来支持矢量图动画。但是5.0以前的版本还是需要引入支持库,也一定程度上增加了包体。所以说可以考虑使用。
二、移除无用资源
这一点主要是使用lint检查并清除冗余资源(Android Studio 选中项目右键 => Analyze => Run Inspection by Name => 输入 unused resuroces)。
如果你的资源是通过资源名称使用Resources的getIdentifier(String name, String defType, String defPackage)方法去获取到资源的id来使用资源(这种方式是通过反射的方法根据资源名称去获取资源的id),而不是直接通过R文件自动生成的id来使用资源的,lint会检测判定你这个文件并没有被使用,而作为未使用的文件列出来。这时候就不能使用一键删除的功能,需要确认后自己手动删除。例如我要使用doodle.png这个资源,一般情况下我们是通过R.drawable.doodle去使用的,如下代码
Resources res = context.getResources();Drawable doodleDrawable = res.getDrawable(R.drawable.doodle);
但是,有些时候我们需要在代码中动态地根据资源名称去使用资源,这时候就要用到getIdentifier()去获取到资源的id,然后再使用这个id去获取资源,示例代码如下:
Resources res = context.getResources();int doodleDrawableId = res.getIdentifier("doodle","drawable", context.getPackageName());Drawable doodleDrawable = res.getDrawable(doodleDrawableId);
这种情况就不能使用一键删除的功能,需要确认后自己手动删除。
三、使用APK Splits对APK进行拆分
APK Splits能让应用程序更有效地构建一些形式的多个apk。使用Splits构建出来的APK是只含有不同的单套资源但功能用途一样的APK。
APK Splits支持两个维度:
屏幕密度 ABI
简而言之,APK Splits就是我们要根据用户的手机去提供基于不同屏幕分辨率(xxhdpi,mhdpi等),so库版本的单个APK,并且应用市场支持发布这种多个APK的功能(即要求应用市场能根据用户的手机的屏幕分辨率,CPU的架构而为用户选择对应的版本的APK提供下载)。这做法目前只适用于海外市场,因为目前只有GooglePlay支持这种Multiple APK Support发布功能。不过我们可以个根据它的知识做一些我自己想做的事:
按屏幕密度拆分,配置代码如下:
android {
splits {
density {
enable true
exclude "ldpi", "tvdpi", "xxxhdpi"
compatibleScreens 'small' , 'normal' , 'large' , 'xlarge'
}
}
enable: 启用屏幕密度拆分机制
exclude: 默认情况下,不设置这个属性所有屏幕密度都包括在内,如果设置,则显式声明移除一些密度。
include: 表示要包括哪些屏幕密度
reset( ): 重置屏幕密度列表为只包含一个空字符串 (这能够实现,在与include一起使用时可以表示使用哪一个屏幕密度,而不是要忽略哪一些屏幕密度)
compatibleScreens:表示兼容屏幕的列表。这将会注入到manifest中匹配的 节点。这个设置是可选的。
构建完成后可以在out/apk/目录下看到多个版本的APK。
按 ABI 拆分:
android {
splits {
abi {
enable true
reset()
include'x86','armeabi-v7a','mips'universalApk true
}
}
}
enable: 启用ABI拆分机制
exclude: 不使用这个属性默认情况下所有ABI都包括在内,可以指明移除一些ABI。
include:指明要包含哪些ABI
reset():重置ABI列表为只包含一个空字符串(这可以实现,在与include一起使用来可以表示要使用哪一个ABI,而不是要忽略哪一些ABI)
universalApk:指示是否打包一个通用版本(包含所有的ABI)。默认值为 false。
四、减resources.arsc文件
简单介绍下resources.arsc文件来源与作用:除了assets和res/raw资源被原装不动地打包进APK之外,其它的资源都会被编译或者处理。除了assets资源之外,其它的资源都会被赋予一个资源ID。打包工具负责编译和打包资源,编译完成之后,会生成一个resources.arsc文件和一个R.java,前者保存的是一个资源索引表,后者定义了各个资源ID常量,供在代码中索引资源。
所有的png文件是以STORE的方式存储到apk里的,关于zip里的STORE和DEFLATE,详见:Zip (file format)
通俗的说,当文件是STORED的方式存储到zip,表示这个文件并没有经过压缩,如果是Defl:N的方式,表示通过DEFLATED normal的方式压缩存储到zip。
现在业内有一个开源的插件针对以上原理进行了一定的压缩,就是下面要讲的
微信资源压缩插件:AndResGuard(这推荐使用,根据我们上一个项目的经验,10MApk压缩掉了将近1M)
其原理就是:
(1)对资源(png, xml, jpg等)名称混淆,资源路径名称混淆以及名称长度压缩
(2)将原来以STORED形式存储到zip中的png文件改成DEFLATED(普通压缩存储)方式。
其Gitghub地址为(里面有详细的接入流程):Android资源混淆工具使用说明
五、压缩assets文件夹
assets目录可以存字体文件、WEB页面、配置文件、图片文件。除了配置文件之外,其它的文件我们都可以进行适当的压缩处理:
字体文件:可以使用字体资源文件编辑神器Glyphs进行压缩,其压缩方式其实就是通过删除不需要的字符从而减少APK的大小。
WEB页面:可以考虑使用7zip压缩工具对该文件进行压缩,在正式使用的时候解压
图片文件:还是使用我们的Tinypng在线工具
六、减少lib文件夹
目前市场上移动端主流的架构主要是arm架构,所以可以考虑不支持x86和mips架构,而CPU是x86或mips架构的手机使用放在arm目录下的兼容so库:
还有arm架构中的eabi-v7a相比于eabi只是在图形渲染方面有了很大的改进,所以如果so库对图形渲染没有很高的要求的话,完全可以把so库只存放在arm eabi目录中,减少eabi-v7a
下面说下配置起,在 build.gradle 使用 abiFilters DSL 配置需要用到的 CPU 架构,并将不需要兼容的 so 文件从项目中移除。
defaultConfig {
ndk{
// 设置支持的so库
abiFilters 'armeabi', 'x86'
}
}
七、资源联网加载
这种方式本质是通过将APK里面的资源文件外置到服务器上,在需要时候再进行加载。这种方案最常用的应用的是游戏;如果是工具性应用或者业务型应用需要一些变通:
1、将一些对性能要求不高,或者UI变换频繁的页面考虑使用H5实现。这种做法的前提是需要优化WebVIew的缓存功能,以便从第二次开始,每次开启页面都可以做到快速渲染,不影响交互体验;
2、使用插件化开发。插件化开发,在动态加载和动态修复技术框架基础上,将业务抽成多个独立的插件APk;