当前位置:K88软件开发文章中心电脑基础基础应用26 → 文章内容

深度剖析 | 阿里热修复如何精简优化补丁资源?

减小字体 增大字体 作者:华军  来源:华军资讯  发布时间:2019-4-24 15:42:49

原标题:深度剖析 | 阿里热修复如何精简优化补丁资源?阿里妹导读:Sophix 是阿里推出的史上首个非侵入式移动热更新解决方案,自去年推出已有一年的时间了。这一年来,阿里集团内外成千上万的app踊跃接入。由于接入简便,操作流畅,功能可靠,资源占用极小,Sophix得到了广大开发者的好评,网上也出现了大量开发者亲身实践的接入文章。今天,我们选取了其中一个改进点——资源补丁的精简优化,来详细介绍一下 Sophix 背后的技术。这一年,关于Sophix热修复我们陆续做了很多优化和改进,包括:Sophix热修复中的资源修复我们在《深入探索Android热修复技术原理》(在阿里技术公众号,回复“热修复”,即可免费下载)书中已经有过介绍,主要思想就是将新增和修改的资源打包到补丁资源包中,以0x66的包名来重新编排这些资源。对比其他热修复需要替换完整资源包,Sophix的增量的资源补丁方案能做到资源补丁最小化,并且运行时无需合成完整资源,实现了性能与空间的最优化。在此基础上,我们继续改进了资源补丁,对resources.arsc中的字符串池进行裁剪,在不损耗运行时性能的情况下让补丁包大小精简到了极致。resources.arsc结构resources.arsc文件集结了所有带id的资源项,其粗略概貌可以由以下这张图展现:这里我们不需要太关注细节,只大致说明一下。每个arsc文件的开头是一个类型为RES_TABLE_TYPE的ResTable_header结构头,它指定了这个arsc文件所包含的其他结构,一般来说,只有一个全局字符串池和其他包资源块,通常情况下(Android Studio默认编译出来的)也仅有一个包,包id为0x7f,也就是说该包下的所有资源编号都是0x7fXXXXXX。我们发现,每个包中还有两个字符串池,分别是类型字符串池和资源项字符串池,这两个字符串池和全局字符串池又有怎样的关系呢?类型字符串池只表示类型对应的名称,像layout、string、color、integer等这些字符串,在arsc中只有一个类型id(比如0、1、2、3等)来表示他们。下面还有例子会详细解释。类型字符串池是比较独立的,而且所占空间很小,与其他结构也没有太大关联。而资源项字符串池中存储的是键字符串,与全局字符串池中存储的是值字符串相对应。这里的键和值就是我们通常理解中键值对(Key-Value)的键和值。之所以值字符串放在全局,应该是Android在设计之初打算在一个resources.arsc中的各个包中进行资源值的复用,然而由于目前默认只有一个0x7f包,自然也没有复用这一说了。只看这个结构会比较抽象,我们举个例子,对于以下这个字符串资源:假设这个资源在编译进arsc之后,对应的id为0x7f010000此时arsc中0x7f包中类型字符串池是0x7f包中键字符串池是arsc文件中的全局值字符串池是那么,在解析这个资源项的时候,由于它的包id为0x7f,就会找到这个0x7f包中来解析,类型id为0x01,表示类型字符串池的第0x01个字符串,也就是这里的string类型,剩下的0x0000,表示该类型的第0个资源项。我们从第0个资源项中解析出它是一个字符串类型的资源(这里省略解析过程),并且得到他的key值为0x1,value的值为0x3。而从前面列出的信息中可以看到,键字符串池第1个字符串为app_name,值字符串池的第3个字符串为MyDemo。由此就可以得到这个MyDemo资源的完整信息了。这里我们可以看出,一个资源中占空间最大的正是字符串池,其他结构只是一些索引数字,所占空间很小,因此如果能对字符串池进行精简,将节省很多空间。字符串池的构造首先,我们得先弄清字符串池的结构是怎样的,它的关键入口是ResStringPool_header这个结构头,系统会以通过这个结构头解析出完整的字符串池。接下来我们从StringPool解析过程的系统源码入手,探寻其具体的构造。核心解析逻辑在ResStringPool::setTo,简单起见,以下代码去掉了与主流程无关的检查代码:这里很清楚地展示了解析的过程,对ResStringPool的各个字段进行赋值。其中有几个比较重要的字段:mEntries与mEntryStyles保存是都是每个字符串在字符串块中的偏移,字符串块就是所有字符串的集合,以分割开,通过偏移可以获得具体的某个字符串值,这个过程体现在另一个ResStringPool::stringAt函数:这里需要注意的一点是,字符串池中的字符串可以以UTF8或者UTF16编码来存储,不同编码中的保存偏移的方式有所不同。这里仅看UTF16的情况,参数idx表示我们要获取的第几个字符串,mEntries[idx/sizeof(uint16_t)可以获得第idx个字符串在字符串池中的偏移off,然后由mStrings+off就可以获得这个字符串实体的起始位置,接着就可以由decodeLength方法得到真正的字符串值。style即表示字符串的样式,后面我们会详细讲到。通过这个解析过程,我们可以得到这张结构图,其很好地体现出字符串池的构造:精简思路我们的资源补丁方案中,补丁中只包含新增和修改的资源,而生成补丁需要一个新包APK和一个旧包APK,毫无疑问,这两种加入补丁包的资源实际上都是属于生成补丁时的新包中的资源,因此直接拿新包APK中resources.arsc的完整字符串池就可以作为补丁的字符串池,我们最早的资源补丁就是直接采用这种方式。这么做有一个好处,就是新增和修改的资源用到的字符串索引完全不需要修改,就可以正常获取到字符串池的具体值。但是,由于字符串池是从完整的新包中直接拿过来的,因此,里面非新增和修改的资源所用的字符串也直接包含在了其中,而这些字符串对于补丁,是多余的。因此,我们需要精简去除的,正是这些无用的字符串。具体来说,主要分为三个步骤:确定要留下的字符串需要留下的字符串,无疑就是补丁资源中使用的字符串,而补丁资源中使用的字符串,就是我们通过比较新包和旧包,得到的新增和修改的资源所用到的字符串。具体来说,我们已经通过比较得到了一个映射表,里面记录了所有新包资源到补丁资源的id映射关系,如下所示:这里需要处理两个字符串池,全局的值字符串池0x7f和包中的键字符串池,其中的无用的字符串和样式都需要去掉。对于0x7f包中的键字符串,我们需要收集表中所有资源的键,也就是这些资源项的名称,得到一个字符串索引值的列表,这个时候得到的列表,由于是新包字符串池的索引,因此是零散分布的。我们可以直接为每个收集到的键的字符串索引重新指定一个索引值,由此得到一

[1] [2]  下一页


深度剖析 | 阿里热修复如何精简优化补丁资源?