作者|张祖桥前言目前,Android端针对包大小的优化方案已经有如江湖之鲫。我们系列的上一篇文章介绍了Class字节码优化,本期我们将以资源文件为中心,从资源二进制文件的新角度开拓包大小优化的新思路。在资源文件优化方面,通常的优化手段多集中在图片/文件压缩、资源文件名混淆、资源文件离线下载等方面,而我们的新思路是基于对常规思路的深入分析和思考。一开始,我们从资源文件名混淆优化入手。业界最著名的资源文件名混淆解决方案开源项目是AndResGuard。本项目的优化目标是资源文件目录res下的文件。优化点:资源文件,通过计算md5值判断是否重复,只保留一份;缩短资源文件的名称,即名称混淆;对APK中的内容采用7zip压缩优化;按照这个项目优化,整体收益可以达到非常可观的MB水平。但是这个项目优化完成后,资源文件的进一步优化就会遇到瓶颈。为了在此基础上更好地优化资源大小,我们需要了解资源文件目录res包含的文件类型和大小分布。以抖音为例,下表列出了子文件夹的名称、文件个数、压缩文件夹的大小,按照文件个数降序排列:从上表中,您可以参见:drawable-xxhdpi-v4目录最大文件数6000+,压缩后文件大小约19.5MB。drawable目录下的文件数排名第三,有4388个文件,压缩后4.6MB,包括图片和.xml文件。第二个和第四个文件是布局目录下的布局文件,分别有5970个和2985个文件,压缩文件夹大小分别为12.2MB和8.5MB。布局文件总数近9K,文件大小约20.7MB。可以看出layout目录下的layout文件大小与image文件不相上下。这么大的文件这部分,除了文件名的混淆优化,还有没有其他的优化方法?还是文件名混淆彻底?另外,APK解压后的resources.arsc文件大小为7.3MB。它包含了app的所有资源文件名和资源字符串值。是否有多余的字符串?对于layout布局文件,从近万个文件的数量和20+MB的文件大小来看,是值得查询的。我们分析了资源文件的二进制文件格式,从正在使用的文件内容的角度分析,发现有多余的内容可以删除。经过对各种稳定性和打包兼容性问题的反复尝试和解决,最终开发出一套针对AndroidARSC/XML文件格式的包大小优化方案,目前已经实现,实现收益2MB多。接下来,本文将深入讲解该方案的实现细节。APK资源格式优化我们的核心思想是以缩短资源路径为优化起点。在最终的APK文件中,从resources.arsc和layout布局文件的二进制文件格式入手,检查其内容结构,找到不用的文件,可以删除。String,优化文件名或文件中的字符串池。主要分为以下两个优化点。资源路径缩短资源格式修改接入AndResGuard后,资源文件res目录->r,里面的子文件夹和文件名也乱了,即:res/anim/abc_fade_in.xml->r/a/a.xmlres/anim/abc_fade_in.xml->r/a/a.xml这是为了减少资源文件路径,从而减小包体积。自然是自然关联,资源文件路径能不能再缩减?显然,如果能把所有文件都放到r目录下,去掉中间的子文件夹,可以进一步减少资源文件路径和zip节点数,对包体肯定有好处;顺便说一句,文件名的后缀也可以去掉,文件路径也可以减少,即:r/a/a.xml->r/ar/a/b.png->r/b由于资源文件名需要修改,resources.arsc文件需要修改。下面分析一下resources.arsc的文件格式:可以看到,它包含3个字符串池。如果我们在res/anim目录下有一个资源文件abc_fade_in.xml,那么resources.arsc文件中的三个字符串池中的信息如下:全局字符串池(字符串池1):主要包含完整的文件路径名,即res/anim/abc_fade_in.xmltypestringpool(stringpool2):资源类型名称(包括res目录下的子文件目录名),即animkeystringpool(stringpool3)):文件名,即:abc_fade_in可以看到有两个地方和资源文件名有关。全局字符串池保存完整的文件路径名,关键字符串池保存文件名。为了缩短资源路径,需要同时修改这些。两个地方,即修改全局字符串池中的res/anim/abc_fade_in.xml->r/f,修改关键字符串池中的abc_fade_in->f。resources.arsc文件中有两个字符串池需要修改,如下图箭头所示:但是缩短资源路径后,发现包大小增加了160K+!我们知道key常量池被切了,文件名被混淆了,其混淆名称来自于符合文件名规范的混淆字符串集,其中的字符串都是唯一且不重叠的,因此字符串集的数量越多,最长字符串的长度就越大。在不缩短资源路径的情况下,可以每次从混淆字符串集合中重新选择不同子目录文件夹中使用的文件名,使其名称在关键字符串池中始终保持最短;其对应的文件名字符串集合为:[a,b,c,d,e]并且在缩短的情况下,由于所有文件都包含在一个文件夹r中,所以使用的文件名只能来自同一组混淆后的字符串,名称会在关键字符串池中逐渐变长,路径字符串也会变长,导致整体结果变大!如下图:对应的文件名字符串集合为:[a,b,c,d,e,f,g,h,i,j]因此,当所有文件都包含在一个文件夹r中时,该文件不同子目录下的名字不能重复使用,所以虽然路径缩短会让全局字符串池变小,但是关键字符串池反而变大了。这是因为键名默认需要和文件名保持一致。猜测:在resources.arsc文件中,键名是需要和文件名保持一致,还是键名本身是必须的?其实在编译之后,资源文件被使用的地方会被替换成一个特定的id值,例如:setContentView(R.layout.activity_main);//=>setContentView(0x7f0b001c);//替换为id值}}由此可见,资源文件名必须与整型id值一一对应。这种一对一的映射关系可以关联到:是否只根据整型id值Name就能找到对应的文件路径?因为这个过程中涉及到的key字符串是绝对没有引用的。基于这个想法,我们将所有的关键字符串池替换为单个值“_”,发现APK工作正常。显然,移除keystringpool似乎并不会影响APK运行时根据整数id值搜索文件路径。那么,关键字符串池中的字符串有什么作用呢?翻阅源码发现,只有通过类似于“资源文件反射”的方式调用,才能获取key字符串池中的字符串值,如://MainActivity.java//这里的返回值是“_”,因为keystringpool已经被“_”替换了"找不到类型"anim"intid=getResources().getIdentifier("abc_fade_in","anim","cn.pkg");目前项目中,一般没有使用上述的“资源文件反射”获取资源名称的使用方法,所以key字符串pool都可以换成单值“_”;目前已知的必须以这种方式使用资源文件的方式多为插件等。在宿主项目的情况下,如果需要,可以保留这部分字符串名称,配置白名单。下图是resources.arsc文件中keystringpool的格式和内容示意图:offset数组(标记1),数组的值指向keystringpool中每个字符串的offset值(mark2)由于需要将key字符串池中的所有字符串替换为单个值“_”,那么key字符串池中将只有一个“_”字符串,offset数组也将只有一个元素,指向key字符串池"_"字符串的起始偏移值为0.最后需要替换resources.arsc文件中key字符串对应的offset数组的索引值,全部调用放置,以字符串“_”对应的偏移数组的索引值为0,这样原来的文件名字符串将被替换为“_”,key字符串池中只剩下“_”字符串。崩溃及兼容性问题项目灰度实现出现崩溃,发现drawable目录下的xml图片文件后缀勾选,如下图:frameworks/base/core/java/android/content/res/ResourcesImpl.java//createdrawableprivateDrawableloadDrawableForCookie(@NonNullResourceswrapper,@NonNullTypedValuevalue,intid,intdensity){...if(file.endsWith(".xml")){//解析并创建xml文件drawablefinalStringtypeName=getResourceTypeName(id);if(typeName!=null&&typeName.equals("color")){dr=loadColorOrXmlDrawable(wrapper,value,id,density,file);}else{dr=loadXmlDrawable(wrapper,value,id,density,file);}}else{//为.png和其他图像解析和创建可绘制对象finalInputStreamis=mAssets.openNonAsset(value.assetCookie,file,AssetManager.ACCESS_STREAMING);最终的AssetInputStreamais=(AssetInputStream)是;dr=decodeImageDrawable(ais,wrapper,value);}...}因此,我们不去掉drawable目录下的.xml后缀。上线后反馈部分手机6.x启动慢。经排查,发现图片文件名后缀被删除优化,导致app在部分rom上启动缓慢。排除这些兼容性问题,最后我们只保留了路径缩短和键常量池剪枝优化,没有去掉文件名后缀,即:r/a/a.xml->r/a.xml,这部分资源路径压缩优化收益300K+。布局优化我们知道布局目录下的布局文件占用的包体积很大。从前面的分析来看,resources.arsc文件中有几个字符串池。有些字符串池用不到,可以删除,而layoutlayout文件的二进制文件格式和resources.arsc文件一样,里面也有一个字符串池。有没有类似的优化点?为此,需要探索布局文件的文件格式和内容,随意打开一个布局文件。其源代码和二进制文件格式如下:
