通常来说,当应用处于成长期的中后阶段时,才会考虑去做系统的包体积优化,因此,只有在这个阶段及之后,包体积优化带来的收益才是可观的。
众所周知,Android构建工具链中使用了AAPT/AAPT2工具来对资源进行处理,Manifest、Resources、Assets的资源经过相应的ManifesMerger、ResourcesMerger、AssetsMerger资源合并器将多个不同moudule的资源合并为了MergedManifest、MergedResources、MergedAssets。然后,它们被AAPT处理后生成了R.java、ProguardConfiguration、CompiledResources。如下图左上方所示:
其中ProguardConfiguration、CompiledResources的作用如下所示:
APK的资源主要包括图片、XML,与冗余代码一样,它也可能遗留了很多旧版本当中使用而新版本中不使用的资源,这点在快速开发的App中更可能出现。我们可以通过点击右键,选中Refactor,然后点击RemoveUnusedResource=>preview可以预览找到的无用资源,点击DoRefactor可以去除冗余资源。如下图所示:
需要注意的,AndroidLint不会分析assets文件夹下的资源,因为assets文件可以通过文件名直接访问,不需要通过具体的引用,Lint无法判断资源是否被用到。
此外,当我们通过shrinkResourcestrue来开启资源压缩,资源压缩工具只会把无用的资源替换成预定义的版本而不是移除。那么,如何高效地对无用资源自动进行去除呢?
我们可以在Android构建工具执行package${flavorName}Task之前通过修改CompiledResources来实现自动去除无用资源,具体的实现原理如下:
通过查看Zip格式资源包中每个ZipEntry的CRC-32checksum来寻找被替换的预定义资源,预定义资源的CRC-32定义在ResourceUsageAnalyze中,如下所示:
具体的实现代码如下所示:
aaptOptions{cruncherEnabled=false}此外,我们还要注意对图片格式的选择,对于我们普遍使用更多的png或者是jpg格式来说,相同的图片转换为webp格式之后会有大幅度的压缩。对于png来说,它是一个无损格式,而jpg是有损格式。jpg在处理颜色图片很多时候根据压缩率的不同,它有时候会去掉我们肉眼识别差距比较小的颜色,但是png会严格地保留所有的色彩。所以说,在图片尺寸大,或者是色彩鲜艳的时候,png的体积会明显地大于jpg。
下面,我们就着重讲解下如何针对性地选择图片格式。
在GoogleI/O2016中,讲到了如何选择相应的图片格式。首先,如果能用VectorDrawable来表示的话,则优先使用VectorDrawable;否则,看是否支持WebP,支持则优先用WebP;如果也不能使用WebP,则优先使用PNG,而PNG主要用在展示透明或者简单的图片,对于其它场景可以使用JPG格式。简单来说可以归结为如下套路:
VD(纯色icon)->WebP(非纯色icon)->Png(更好效果)->jpg(若无alpha通道)
用图形化的形式如下所示:
最后,如果要在项目中使用VD,则以下几点需要着重注意:
与VD类似,还有一种矢量图标iconFont,即字体图标,图标就在字体文件里面,它看着是个图标,其实却是个文字。它的优势有如下三个方面:
它的缺点也很明显,大致有如下三个方面:
如果不是纯色小icon类型的图片,则建议使用WebP。只要你的App的minSdkVersion高于14(Android4.0+)即可。WebP不仅支持透明度,而且压缩率比JPEG更高,在相同画质下体积更小。但是,只有Android4.2.1+才支持显示含透明度的WebP,此外,它的兼容性不好,并且不便于预览,需使用浏览器打开。
此外,在Gradle构建APK的过程中,我们可以判断当前App的minSdkVersion以及图片文件的类型来选用是否能使用WebP,代码如下所示:
然后,我们来讲解下资源如何进行混淆。
同代码混淆类似,资源混淆将资源路径混淆成单个资源的路径,这里我们可以使用AndroidResGuard,它可以使冗余的资源路径变短,例如将res/drawable/wechat变为r/d/a。
下面,我们就使用AndroidResGuard来对资源进行混淆。
APK生成目录如下:
对于AndResGuard工具,主要有两个功能,一个是资源混淆,一个是资源的极限压缩。下面,我们就来分别了解下它们的实现原理。
资源混淆工具主要是通过短路径的优化,以达到减少resources.arsc、metadata签名文件以及ZIP文件大小的效果,其效果分别如下所示:
AndResGuard使用了7-Zip的大字典优化,APK的整体压缩率可以提升3%左右,并且,它还支持针对resources.arsc、PNG、JPG以及GIF等文件进行强制压缩(在编译过程中,这些文件默认不会被压缩)。那么,为什么Android系统不会去压缩这些文件呢?主要基于以下两点原因:
我们可以通过内联RField来进一步对代码进行瘦身,此外,它也解决了RField过多导致MultiDex65536的问题。要想实现内联RField,我们需要通过Javassist或者ASM字节码工具在构建流程中内联RField,其代码如下所示:
我们可以把所有的资源文件合并成一个大文件,而一个大资源文件就相当于换肤方案中的一套皮肤。它的效果比资源混淆的效果会更好,但是,在此之前,必须要解决解析资源与管理资源的问题。其相应的解决方案如下所示:
我们需要根据App目前所支持的语言版本去选用合适的语言资源,例如使用了AppCompat,如果不做任何配置的话,最终APK包中会包含AppCompat中所有已翻译语言字符串,无论应用的其余部分是否翻译为同一语言。对此,我们可以通过resConfig来配置使用哪些语言,从而让构建工具移除指定语言之外的所有资源。同理,也可以使用resConfigs去配置你应用需要的图片资源文件类,如"xhdpi"、"xxhdpi"等等,代码如下所示:
android...defaultConfig{ ...resConfigs"zh","zh-rCN"resConfigs"nodpi","hdpi","xhdpi","xxhdpi","xxxhdpi"}...}此外,我们还以利用DensitySplits来选择应用应兼容的屏幕尺寸大小,代码如下所示:
android{...splits{density{enabletrueexclude"ldpi","tvdpi","xxxhdpi"compatibleScreens'small','normal','large','xlarge'}}...}9、尽量每张图片只保留一份比如说,我们统一只把图片放到xhdpi这个目录下,那么在不同的分辨率下它会做自动的适配,即等比例地拉伸或者是缩小。
我们可以将一些图片资源放在服务器,然后结合图片预加载的技术手段,这些既可以满足产品的需要,同时可以减小包大小。
如设定统一的字体、尺寸、颜色和按钮按压效果、分割线shape、selector背景等等。
对于主要由C/C++实现的NativeLibrary而言,常规的优化方式就是去除Debug信息,使用C++_shared等等。下面,对于So瘦身,我们看看还有哪些方案。
So是Android上的动态链接库,在我们Android应用开发过程中,有时候Java代码不能满足需求,比如一些加解密算法或者音视频编解码功能,这个时候就必须要通过C或者是C++来实现,之后生成So文件提供给Java层来调用,在生成So文件的时候就需要考虑生成市面上不同手机CPU架构的文件。目前,Android一共支持7种不同类型的CPU架构,比如常见的armeabi、armeabi-v7a、X86等等。理论上来说,对应架构的CPU它的执行效率是最高的,但是这样会导致在lib目录下会多存放了各个平台架构的So文件,所以App的体积自然也就更大了。
因此,我们就需要对lib目录进行缩减,我们在build.gradle中配置这个abiFiliters去设置App支持的So架构,其配置代码如下所示:
defaultConfig{ndk{abiFilters"armeabi"}}一般情况下,应用都不需要用到neon指令集,我们只需留下armeabi目录就可以了。因为armeabi目录下的So可以兼容别的平台上的So,相当于是一个万金油,都可以使用。但是,这样别的平台使用时性能上就会有所损耗,失去了对特定平台的优化。
看到上图中的libimagepipeline_x86.so,下面我们就以这个so为例来写写加载它的伪代码,如下所示:
在Android4.3(API17)之前,单个进程加载的SO数量是有限制的,在Google的linker.cpp源码中有很明显的定义,如下图所示:
至此,可以看到,FaceBook出品的Buck同ReDex一样,里面的功能都十分强大,Buck除了实现LibraryMerge和Relinker功能之外,还实现了三大功能,如下所示:
如果有相应需求或对Buck感兴趣的同学可以去看看它们的实现源码。
我们需要回顾过去的业务,合理地去评估并删除无用或者低价值的业务。
如果所有的功能都不能移除,那就可能需要去转变开发模式,比如可以更多地采用H5、小程序这样开发模式。
对于应用包体积的监控,也应该和内存监控一样,去作为正式版本的发布流程中的一环,并且应该尽量地去实现自动化与平台化。(这里建议任何大于100kb的功能都需要审批,特别是需要引入第三方库时,更应该慎重)
包体积的监控,主要可以从如下三个纬度来进行:
瘦身优化是性能优化当中不那么重要的一个分支,不过对于处于稳定运营期的产品会比较有帮助。下面我们就来看看对于瘦身优化有哪些常见问题。
我们在回答的时候要注意一些可操作的干货,同时注意结合你的项目周期。主要可以从以下三点来回答:
在项目初期,我们一直在不断地加功能,加入了很多的代码、资源,同时呢,也没有相应的规范,所以说,UI同学给我们很多UI图的时候,都是没有经过压缩的图片,长期累积就会导致我们的包体积越来越大。到了项目稳定期的时候,我们对各种运营数据进行考核,发现APK的包大小影响了用户下载的意愿,于是我们就着手做包体积的优化,我们采用的是AndroidStudio自带的AnalyzeAPK来做的包体积分析,主要就是做了代码、资源、So等三个方面的重点优化。
首先,针对于代码瘦身,第一点,我们首先使用Proguard工具进行了混淆,它将程序代码转换为功能相同,但是不容易理解的形式。比如说将一个很长的类转换为字母a,同时,这样做还有一个好处,就是让代码更加安全了。第二点呢,我们将项目中使用到的一些第三方库进行了统一,比如说图片库、网络库、数据库等,不允许项目中出现功能相同,但是却实现不一样的库。同时也做了规范,之后引入的三方库,需要去考量它的大小、方法数等,而且呢,如果只是需要一个很大库的一个小功能,那我们就修改源码,只引入部分代码即可。第三点,我们将项目中的无用代码进行了删减,我们使用了AOP的方式统计到了哪些Activity以及fragment在真实的场景下没有用户使用,这样你就可以删除掉了。对于那些不是Activity或者是Fragment的类,我们切了很多类的构造函数,这样你就可以统计出来这些类在线上有没有真正被调用到。但是,对于代码的瘦身效果,实际上不是很明显。
接下来,我们做了资源的瘦身。首先,我们移除了项目当中冗余的资源文件,这一点在项目当中一定会遇到。然后,我们做了资源图片的压缩,UI同学给我们资源图片的时候,需要确认已经是压缩过的图片,同时,我们还会做一个兜底策略,在打包的时候,如果图片没有被压缩过,那我们就会再来压缩一遍,这个效果就非常的明显。对于资源,我们还做了资源的混淆,也就是将冗余的资源名称换成简短的名字,资源压缩的效果要比代码瘦身的效果要好的多。
最后,我们做了So的瘦身。首先,我们只保留了armeabi这个目录,它可以兼容别的CPU架构,这点的优化效果非常的明显。移除了对别的架构适配So之后,我们还做了另外一个处理,对于项目当中使用到的视频模块的So,它对性能要求非常高,所以我们采用了另外一种方式,我们将所有这个模块下的So都放到了armeabi这个目录下,然后在代码中做判断,如果是别的CPU架构,那我们就加载对应CPU架构的So文件即可。这样即减少了包体积,同时又达到了性能最佳。最后,通过实践可以看出So瘦身的效果一般是最好的。
主要可以从以下两个方面来进行回答:
在大型项目中,最好的方式就是结合CI,每个开发同学在往主干合入代码的时候需要经过一次预编译,这个预编译出来的包对比主干打出来的包大小,如果超过阈值则不允许合入,需要提交代码的同学自己去优化去提交的代码。此外,针对项目的架构,我们可以做插件化的改造,将每一个功能模块都改造成插件,以插件的形式来支持动态下发,这样应用的包体积就可以从根本上变小了。
至此,我们可以了解到,如果要想对包体积做更深入的优化,就必须对APK组成,Dex、So动态库以及Resource文件格式,还有APK的编译流程有深入地了解,这样我们才能有足够的内功素养去实现包体积的深度优化。