业务方和开发都希望app尽量的小,本文会给出多个实用性的技巧来帮助开发者进行app的瘦身工作。瘦身和减负虽好,但需要注意瘦身对于项目可维护性的影响,建议根据自身的项目进行技巧的选取。
一、背景
目前app的大小越来越大,用户对于过大的app接受度不高,所以除了插件化和RN的方案外,我们只能老老实实的进行app的瘦身工作。
二、需求
-
我要利用混淆来让我的代码尽可能少
-
最好能用最少的切图完成功能
-
layout文件不要太多,太多了乱
-
能动态下载的就做动态
-
我希望能用大小最小的图片
-
如果能用svg,我就用svg
-
对于无用的资源,我要as能自动删除掉
-
中国文字博大精深,而我只要我需要的字的字体
-
最好能根据下载用户手机的cpu和分辨率来引入不同的资源
三、实现
分析app组成结构
做瘦身之前一定要了解自己app的组成结构,要有针对性的进行优化,并且要逐步记录比对,这样才能更好的完成此项工作。关于apk的大小,我推荐google的这个视频。目前as的2.2预览版中已经有了apk分析器,功能相当强大,此外你还可以利用nimbledroid来分析apk。
nimbledroid是一个强大的工具,推荐一试我们都知道apk是由:
-
asserts
-
lib
-
res
-
dex
-
META-INF
-
androidManifest
这几个部分构成的。
下面我会利用as的分析工具,以微信、微博、淘宝为例进行讲述。
分析完成后你还可以看到具体类目占的百分比,清晰明了。旁边的“对比”按钮提供了diff的功能,让你可以方便的进行apk优化前后的对比,简直利器。
assets
assets目录可以存放一些配置文件或资源文件,比如webview的本地html,react native的jsbundle等,微信的整个assets占用了13.4M。如果你的应用对本地资源要求很少的话,这个文件应该不会太大。
lib
lib目录下会有各种so文件,分析器会检查出项目自己的so和各种库的so。微博和微信一样只支持了arm一个平台,淘宝支持了arm和x86两个平台。
resources.arsc
这个文件是编译后的二进制资源文件,里面是id-name-value的一个map。因为微信做了资源的混淆,所以这里可以看到资源名称都是不可读的。
索性放个微博的图,易于大家理解:
META-INF
META-INF目录下存放的是签名信息,用来保证apk包的完整性和系统的安全性,帮助用户避免安装来历不明的盗版apk。
res
res目录存放的是资源文件,包括图片、字符串。raw文件夹下面是音频文件,各种xml文件等等。因为微信做了资源混淆,图片名字都不可读了。
微博就没有做资源混淆,所以可读性较好:
dex
dex文件是java代码打包后的字节码,一个dex文件最多只支持65535个方法,这也是为什么微信有了三个dex文件的原因。
因为dex分包是不均匀的,你可以理解为装箱,一个箱子的大小是固定的,但你代码的量是不确定的,微信把前两个箱子装满了,最后还剩了2m多的代码,这些代码也占用了一个箱子,最终产生了上图不均匀的情况。
现在,我们已经知道了apk中各个文件的大小和它们占的比例,下面就可以开始针对性的进行优化了。
优化assets
assets中会存放资源文件,这个目录中不同厂的app存放的内容各有不同,所以优化也比较难。自从引入RN以来,这个目录下还会有jsbundle的信息(可参考全民k歌)。如果你有地址选择的功能,这里还会存放地址的映射文件。
对于这块的资源,as是不会进行主动的删减的,所以一切都是需要靠开发者进行手动管理的。
删除无用字体
中文字体是相当大的,我一直不建议将字体文件随意丢弃到assets中。有时候一个小功能急着上,开发者为了追求速度,可以先放在这里图省事。但一定要知道这个隐患,并且一定要多和产品核对功能的必要性。对于有些只会用在logo中的字体,我推荐将字体文件进行删减处理。
FontZip是一个字体提取工具,readme中写到:
经过测试,已经把项目5MB的艺术字体,按需求提取后,占用只有20KB,并且可正常使用。
减少icon-font的使用
icon-font和svg都能完成一些icon的展示,但因为icon-font在assets中难以管理,并且功能和svg有所重叠,所以我建议减少icon-font的使用,利用svg进行代替,毕竟一个很小的icon-font也比svg大。这里给出一个提供各种格式icon的网站,方便大家进行测试:https://icomoon.io/app/
-
svg:549字节
-
png:375字节(单一分辨率的一张图)
-
ion-font:1.1kb
动态下载资源
字体、js代码这样的资源能动态下载的就做动态下载,虽然这样会增加出错的可能性,复杂度也会提升,但对于app的瘦身和用户来说是有长远的好处的。
如果你用了rn,你可以在app运行时动态去拉取最新的代码,将图片和js代码一并下载后解压使用。也可以把rn模块化,主线的rn代码随着app发布,入口较深的次要界面可以在app启动后通过断点下载。
压缩资源文件
有些资源文件是必须要随着app一并发布的。对于这样的文件,可以采用压缩存储的方式,在需要资源的时候将其解压使用,下面就是解压zip文件的代码示例:
全民k歌中的assets目录下我就发现了大量的zip文件:
android上也有一个7z库帮助我们方便的使用7z,这个库我目前没用到,有需求的同学可以尝试一下。
优化lib
配置abiFilters
一个硬件设备对应一个架构(mips、arm或者x86),只保留与设备架构相关的库文件夹(主流的架构都是arm的,mips属于小众)可以大大降低lib文件夹的大小。配置方式也十分简单,直接配置abiFilters即可:
armeabi就不用说了,这个是必须包含的,v7是一个图形加强版本(如果用到模糊算法,则不要删除),x86是英特尔平台的支持库。
官方例子:
按 ABI 拆分
-
enable: 启用ABI拆分机制
-
exclude: 默认情况下所有ABI都包括在内,允许移除一些ABI
-
include:指明要包含哪些ABI
-
reset():重置ABI列表为只包含一个空字符串(这也是允许的,在与
include
一起使用来可以表示要使用哪一个ABI) -
universalApk:指示是否打包一个通用版本(包含所有的ABI)。默认值为 false。
根据手机的cpu来引入so
我们在舍弃so之前一定要进行用户cpu型号的统计,这样你才能放心大胆地进行操作。
我先是花了几个版本的时间统计了用户的cpu型号,然后排除了没有或少量用户才会用到的so,以达到瘦身的目的。
注意:
-
如果你和我一样用到了
renderscript
,那么你必须包含v7,否则会出现模糊异常的问题。 -
如果你用了RN,那么对于x86需要谨慎的保留,否则可能会出现用户找不到so而崩溃的情况。毕竟rn是一个全局的东西,稍有不慎就可能会出现开机崩的情况。
-
so这个东西还是比较危险的,我们虽然可以通过统计cpu型号来降低风险,但我还是推荐发布app前走一遍大量机型的云测,通过云测平台把风险进一步降低。
-
小厂的项目可能会舍弃一些so,但随着公司规模的增大,你未来仍旧要重复考虑这个问题。所以我推荐在崩溃系统中上传用户cpu型号的信息,这样我们就可以在第一时间知道因找不到so引起的崩溃量,至于是否需要增加so就看问题的严重程度了。
避免复制so
so有个常年大坑:在Android 6.0之前,so文件会压缩到apk中,系统在安装应用的时候,会把so文件解压到data分区。这样同一个so文件会有两份存在,一个在apk里,一个在data中。这也导致多占用了一倍的空间,而且会出现各种诡异的错误。这个策略虽然和apk的瘦身无关,但它和app安装在用户手机中的大小有关,因此我们也是需要多多留意的。
Starting from Android Studio 2.2 Preview 2 and newest build tools, the build process will automatically store native libraries uncompressed and page aligned in the APK
在6.0 中,可以通过如下的方式进行申明:
如果想了解更多信息或者想知道这种配置的限制,可以浏览下SmallerAPK(8)。
优化resources.arsc
resources.arsc中存放了一个对应关系:
id | name | default | v11 |
---|---|---|---|
0x7f090002 | PopupAnimation | @ref/0x7f040042, @ref/0x7f040041 | … |
我们在程序运行的时候肯定要经常用到id,因此它在安装之后仍需要被频繁的读取。如果将这个文件进行了压缩,在每次读取前系统都必须进行解压的工作。这就会有一些性能和内存的开销,综合考虑下来,压缩这个文件是得不偿失的。
删除无用的资源映射
resources.arsc的正确瘦身方式是删除不必要的string entry
,你可以借助 android-arscblamer来检查出可以优化的部分,比如一些空的引用。
进行资源名称混淆
微信团队开源了一个资源混淆工具,AndResGuard。它将资源的名称进行了混淆,所以可以用它对resources.arsc
进行优化,只是具体优化效果与编码方式、id数量、平均减少命名长度有关。
表1:
id | name | default | v11 |
---|---|---|---|
0x7f090001 | Android | @ref/0x7f040042, @ref/0x7f040041 | … |
0x7f090002 | ios | @ref/0x7f040042, @ref/0x7f040041 | … |
0x7f090003 | Windows Phone | @ref/0x7f040042, @ref/0x7f040041 | … |
表2:
id | name | default | v11 |
---|---|---|---|
0x7f090001 | a | @ref/0x7f040042, @ref/0x7f040041 | … |
0x7f090002 | b | @ref/0x7f040042, @ref/0x7f040041 | … |
0x7f090003 | c | @ref/0x7f040042, @ref/0x7f040041 | … |
我们一眼就可以知道表2肯定比表1存储的字符要小,所以整个文件的大小肯定也要小一些。
详细信息请参考:smallerapk-part-3-removing-unused-resources
关于AndResGuard
这个压缩工具其实就是一个task,使用也十分简单,具体的用法请参考中文文档。
原理介绍:安装包立减1M--微信Android资源混淆打包工具
使用这个工具的时候需要注意一些东西:像友盟这种喜欢用反射获取资源的SDK就是一个坑(友盟的SDK就是坑王!)对于app启动图标这样的icon可以不做混淆,推荐将其放入白名单中。
优化META-INF
META-INF文件夹中有三个文件,分别是MANIFEST.MF、CERT.SF、CERT.RSA。下面我将会列出简要的分析,如果你希望更详尽的了解原理,可以查看《Android APK 签名文件MANIFEST.MF、CERT.SF、CERT.RSA分析》。
MANIFEST.MF
每一个资源文件(res开头)下面都有一个SHA1-Digest的值。这个值为该文件SHA-1值进行base64编码后的结果。
如果要探究原理,可以看下SignApk.java。这个类中的main方法:
上述代码说明了SHA1-Digest-Manifest是MANIFEST.MF文件的SHA1并base64编码的结果。
CERT.SF
这里有一项SHA1-Digest-Manifest的值,这个值就是MANIFEST.MF文件的SHA-1并base64编码后的值。后面几项的值是对MANIFEST.MF文件中的每项再次SHA1并base64编码后的值。所以你会看到在manifest.mf中的资源名称在这里也出现了,比如abc_btn_check_material
这个系统资源文件就出现了两次。
MANIFEST.MF:
CERT.SF
-
前者:4XHnecusACTIgtImUjC7bQ9HNM8=
-
后者:YFDDnTUd6St4932sE/Xk6H0HMoc=
如果你把前一个文件打开在后面加上\n\r,然后进行编码,你就会得到CERT.SF中的值。
CERT.RSA
CERT.RSA包含了公钥、所采用的加密算法等信息。它对前一步生成的MANIFEST.MF使用了SHA1-RSA算法,用开发者的私钥进行签名,在安装时使用公钥解密它。解密之后,将它与未加密的摘要信息(即,MANIFEST.MF文件)进行对比,如果相符,则表明内容没有被修改。
这点和app瘦身就完全无关了,这块我平时也没有仔细研究过,就不误人子弟了。具体的签名过程可以参考:http://blog.csdn.net/asmcvc/article/details/9312123
优化建议
通过分析得出,除了CERT.RSA没有压缩机会外,其余的两个文件都可以通过混淆资源名称的方式进行压缩。
优化res
资源文件的优化一直是我们的重头戏。如果要和它进行对比,上文的META-INF文件的优化简直可以忽略不计。res的优化分为两块:一个是文本资源(shape、layout等)优化和图片资源优化。本节仅探讨除图片资源优化外的内容,关于图片的内容下面会另起一节。
说明:
上图中有-v4,-v21这样的文件有些是app开发者自己写的,但大多都是系统在打包的时候自动生成的,所以你只需要考虑自己项目中的drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi即可。
通过as删除无用资源
在as的任何文件中右击,选择清除无用资源
即可删除没有用到的资源文件。
不要勾选清除id!如果清除了id,会影响databinding的使用(id绝对占不了多少空间)
Tips:
做此操作之前,请务必产生一次commit,操作完成后一定要通过git看下diff。这样既方便查看被删除的文件,又可以利用git进行误删恢复。
打包时剔除无用资源
shrinkResources顾名思义————收缩资源。将它设置为true后,每次打包的时候就会自动排除无用的资源(不仅仅是图片)。有了它的帮忙,即使你忘记手动删除无用的资源文件也没事。
删除无用的语言
大部分应用其实并不需要支持几十种语言的,微信也做了根据地区选择性下载语言包的功能。作为国内应用,我们可以只支持中文。推荐在项目的build.gradle
中进行如下配置:
这样在打包的时候就会排除私有项目、android系统库和第三方库中非中文的资源文件了,效果还是比较显著的。
控制raw中资源的大小
-
assets目录允许下面有多级子目录,而raw下不允许存在目录结构
-
assets中的文件不会产生R文件映射,但raw会
-
如果你app最低支持的版本不是2.3的话,assets和raw应该都不会对资源文件的大小进行限制
-
raw文件会生成R文件映射,可以被as的lint分析,而assets则不能
-
raw缺少子目录的缺点让其无法成为存放大量文件的目录
一般raw文件下会放音频文件。如果raw文件夹下有音频文件,尽量不要使用无损(如:wav)的音频格式,可以考虑同等质量但文件更小的音频格式。
ogg是一种较适合做音效的音频格式。当年我初中做游戏的时候,我全都是用的mp3和png,最终游戏达到了2G。在换为ogg和jpg后,游戏缩小到了1G以内(因为游戏中音频和大图较多,所以效果比较夸张)。
移动端的音频主要是音效和短小的音频,所以淘宝大量选择了ogg格式,微博的选择格式比较多,有wav、mp3、ogg,我更加推荐淘宝的做法。当然,你仍旧不要忘记opus格式,opus也是一种有损压缩格式,如果感兴趣的话也可以尝试一下。
统一应用风格,减少shape文件
一个应用的界面风格是必须要统一的,这个越早做越好,最基本的就是统一颜色和按钮的按压效果。无UI设计和扁平化风格流行后,倒是给应用瘦身带来了极大的的福利。界面变得越朴实,我们可以用shape画的东西就越多。
当你的app统一过每种颜色对应的按下颜色后,接下来就需要统一按钮的形状、按钮的圆角角度、有无阴影的样子、阴影投射角度,阴影范围等等,最后还要考虑是否支持水波纹效果。
我简单将按钮分为下列元素:
元素 | 属性01 | 属性02 | 属性03 | 属性04 |
---|---|---|---|---|
形状 | 正方形 | 三角形 | 圆角矩形 | 圆形 |
颜色 | 红 | 黄 | 蓝 | 绿 |
有无阴影 | 有 | 无 | ||
阴影大小 | 3dp | 5dp | ||
阴影角度 | 90° | 120° | 180° | |
水波纹效果 | 有 | 无 |
各个元素组合后会产生大量的样式,shape和layer-list当然可以实现各种组合,但这样的话光按钮的背景文件就有n个,很不好维护。
一般为了开发方便,都会把需要用到的各种selector图片事先定义好,做业务的时候只需要去调用就行。但这大量的selector文件对于业务开发者来说也是有记忆难度的,所以我推荐使用SelectorInjection这个库,它可以将上面的每个元素进行各种组合,用最少的资源文件来实现大量的按压效果。
用库虽然好,但库也会带来学习成本,所以引入者可以将上述的组合定义为按钮的一个个的style。因为style本身是支持继承的,对于这样的组合形态来说,继承真是是一大利器。当你的style有良好的命名后,调用者只需要知道引入什么style就行,至于你用了什么属性别人才不希望管呢。
如果业务开发中有一些特别特殊的按压状态,没有任何复用的价值,那你就可以利用库提供的丰富属性在layout文件中进行实现,再也不用手忙脚乱的到处定义selector文件了。
我将不能继承和不灵活的shape变成了一个个单一的属性,通过库将多个属性进行组合,接着利用支持继承的style来将多个属性固定成一个配置文件,最后对外形成强制的规范性约束,至此便完成了减少selector文件的工作。
使用toolbar,减少menu文件
menu文件是actionBar时代的产物,as虽然对于menu的支持做的还不错,但我也很难爱上它。
menu的设计初衷是解耦和抽象,但因为过度的解耦和定制,让开发变得很不方便,很多项目已经不再使用menu.xml作为actionbar的菜单了。
就目前的形势来看,toolbar是android未来的方向。我虽然作为一个对actionbar和actionbar的兼容处理相当了解的人,但我还是不得不承认actionbar的时代过去了。如果你不信,我可以告诉你淘宝的menu文件就3个,微博的menu文件就9个,如果你还是苦苦依恋着actionbar的配置模式,我推荐一个库AppBar,它可以让你在用灵活的toolbar的同时也享受到配置menu的便利性。