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

摘要: 这一年,关于Sophix热修复我们陆续做了很多优化和改进,包括: 兼容最新Android版本至Android P dp3 JIT混合编译的兼容 第三方加固的全面兼容 新增稳健接入方式 三星低版本特殊机型的兼容 补丁工具加速与初始化检查 资源补丁深度优化 其他稳定性和性能的改进 Sophix热修复中的资源修复我们在《深入探索Android热修复技术原理》(在阿里技术公众号,回复“热修复”,即可免费下载)书中已经有过介绍,主要思想就是将新增和修改的资源打包到补丁资源包中,以0x66的包名来重新编排这些资源。

这一年,关于Sophix热修复我们陆续做了很多优化和改进,包括:

兼容最新Android版本至Android P dp3

JIT混合编译的兼容

第三方加固的全面兼容

新增稳健接入方式

三星低版本特殊机型的兼容

补丁工具加速与初始化检查

资源补丁深度优化

其他稳定性和性能的改进

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:字符串偏移数组指针

mStringPoolSize:字符串个数

mStrings:字符串块的起始地址

mEntryStyles:样式偏移数组指针

mStylePoolSize:样式个数

mStyles:所有样式的存储的起始地址

mEntries与mEntryStyles保存是都是每个字符串在字符串块中的偏移,字符串块就是所有字符串的集合,以0分割开,通过偏移可以获得具体的某个字符串值,这个过程体现在另一个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包中的键字符串,我们需要收集表中所有资源的键,也就是这些资源项的名称,得到一个字符串索引值的列表,这个时候得到的列表,由于是新包字符串池的索引,因此是零散分布的。


我们可以直接为每个收集到的键的字符串索引重新指定一个索引值,由此得到一张新包索引到补丁包索引的映射表:


对于全局值字符串池的处理也是类似,不同地方在于,我们需要进一步解析每个资源项,得到其对应的具体字符串值,仍然是以这个资源为例:


我们需要找到的,就是app_name在0x7f包键字符串的索引,以及MyDemo在全局值字符串中的索引。

另外,我们还需要处理样式。样式是字符串的特殊格式,比如下面的这个资源


这里的Demo字符串就拥有加粗的样式,而某个字符串对应的样式的在样式表中的索引值与这个字符串在字符串池中的索引值是一样的。aapt在编译的时候也会将带有样式的资源全部放到字符串池的最前面。比如有五个字符串具有样式,这五个字符串就会被默认放到字符串池的前五个,而样式表也只有五个样式,分别对应了这前五个字符串。而从第六个字符串以后,就没有样式了。

所以,这里我们还需要调整样式表,把收集到的字符串所对应的样式也一同移动到对应位置。此外,样式字符串,也就是例子中的b字符串实际上也是保存在字符串池中的,因此,当使用到某个样式的时候,还需要将该样式的字符串索引添加到我们的索引映射表中并重新编排。

重新编排与调整偏移值

我们用一张示意图来描述这个编排过程:


其中深色offset entry的表示补丁中实际有效的字符串所对应的偏移值,可以看到,其中的新包中entries按照前面安排的映射关系移动到了补丁entries的相应位置,并且entries的偏移值也根据新排布的字符串位置进行了调整。下方的字符串块strings和样式块styles的内容也只保留有效部分,这样,所有有效字符串紧贴在了一起,并去除了新包中其他无用的资源,大幅节省了空间。

最后需要重新构造字符串的头部ResStringPool_header结构,使得其中的各个字段(stringCount、styleCount、stringsStart、stylesStart等)填入正确的值。

这样,一个有效的补丁字符串池就完整构建好了。这个重排的过程对于键值两种字符串池是完全相同的。

修正资源引用处

字符串池构建完毕了以后,还需要对资源中使用到这些字符串的地方进行重新索引。显然,只需要根据这个映射表:


把原来的老索引值修正为新索引值就行了。具体来说,就是将资源文件结构中的ResTable_entry(代表资源项)和Res_value(代表具体资源的值)中,类型为ResStringPool_ref的字段的index值修正过来即可。


由于我们压缩优化的是resources.arsc中的字符串池,因此需要完整地遍历每个补丁资源项,把相应的index做替换。而xml中的资源不需要相应修改,因为xml中使用到的只有arsc里面的资源id,感知不到id对应的字符串是什么,所以只要在arsc中处理好,xml自然就能找到id所持有的正确的字符串。

总结

通过这三个步骤,便实现了字符串池的精简。当然处理过程中还有有很多零碎的问题,比如引用类型资源的处理、Map资源项和字符串池各个块的拼接等等,这些都需要十分细致地处理好,否则都会导致运行时解析格式失败而崩溃。本文没有述及这些繁琐的问题,也是为了不因为它们而扰乱了主要处理逻辑,当搞定了主干后,回头再收拾这些细枝末节就显得游刃有余了。

精简后效果是很明显的,不过具体还是取决于原始APK中资源字符串的数量以及补丁资源中实际有效的字符串的数量,如果资源字符串较多的话会有非常显著的优化。我们遇到最极端的一个例子是,精简之前带资源的补丁有4M大小,而精简之后直接变为23K!由此可见一斑。

目前Sophix最新版本打包工具的高级选项中已默认开启这个优化资源补丁选项,立刻使用就能为你的资源热修复补丁瘦身。


当然,还有一些其他选项开关,是为了打包的灵活性而设置的,其中有些强烈建议打开的选项我们已经默认开启了。

Sophix热修复中还有许多技术优化点,我们也在去年7月推出了《深入探索Android热修复技术原理》免费电子书,详细讲解了代码、资源、动态库的热修复实现(在阿里技术公众号,回复“热修复”,即可下载)。值此一周年之际,我们与电子工业出版社合作,计划在近期出版该书的印刷纸质版,并新增了一些篇章,以方便大家翻阅,敬请期待。

本文作者: 万壑
原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/521577.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

开启企业级市场转型之路 群晖亮出安全“杀手锏”

戳蓝字“CSDN云计算”关注我们哦!数据犹如企业经营者的眼睛,通过数据可以反映出很多经营中的问题。随着大数据应用日益渗透到各行各业中,数据所蕴含着的巨大商业价值也逐渐被发掘,通过挖掘分析与管理,释放更大的价值&a…

c语言空格键么 有什么意义,C语言里这个空格键跟'\0'到底啥区别啊?

满意答案为CS而liven2019.11.24采纳率:57% 等级:7已帮助:60人空格是空格,结束符0是结束符0,两者不对等。你贴的那个图是不是讲的scanf输入,scanf这个函数默认是将空格作为分割符号,所以你输入…

运放电路的工作原理_图文讲解!教你看懂7款经典运放电路

引言运放的基本分析方法:虚断,虚短。对于不熟悉的运放应用电路,就使用该基本分析方法。运放是用途广泛的器件,接入适当的反馈网络,可用作精密的交流和直流放大器、有源滤波器、振荡器及电压比较器。1、运放在有源滤波中…

自底向上——知识图谱构建技术初探

摘要: 知识图谱,是结构化的语义知识库,用于迅速描述物理世界中的概念及其相互关系,通过将数据粒度从document级别降到data级别,聚合大量知识,从而实现知识的快速响应和推理。文/阿里安全 染青“The world i…

如何关闭rabbitmq

rabbitmqctl stop方式2 先用ps -ef|grep rabbitmq 查询出进程号,然后用kill -9 进程号,杀死进程RabbitMQ常用命令 说明命令启用Web控制台rabbitmq-plugins enable rabbitmq_management开启服务systemctl start rabbitmq-server.service停止服务system…

全国信息联赛c语言,信息学竞赛之编程规则

本规定适用于NOI系列的各项全国性竞赛。NOI其它规章、规则中所有与本规定不符之处,均以本规定为准。不遵守本规定所造成的不良后果由选手本人承担。评测环境与竞赛环境相同。编程通则1.对于每一道试题,选手只应提交一个源程序文件。源程序文件…

正在播放2020Me比较特别的我_蓝牙耳放还是播放器,工作的人当然是都买之第二番 飞傲M5分享...

前言之说好的都买呢在4月份在入了BTR1后,研究下一个蓝牙耳放时,网上有人推荐过山灵m0,不过当时BTR3正在做活动,并且作为一个耳放来说BTR3更为纯粹,最后还是先入手BTR3并暂缓了m0的购置,在玩腻了BTR3后&…

支撑全网70%世界杯流量 盘点世界杯直播背后的阿里云黑科技

摘要: 上周六晚间的法阿大战精彩纷呈,开场11分钟法国队就首开纪录,随后阿根廷强势逆袭连扳2球,但下半场法国队的年龄优势凸显,帕瓦尔世界波以及姆巴佩梅开二度最终让阿根廷止步16强,梅西再度饮恨世界杯。 这…

教你编写一个机器学习代码也能使用的单元测试

摘要: 想不想节省重新训练数据的时间?想不想让你的研究成果有个质的飞跃?来看看这些单元测试,助你一臂之力。注:这篇文章自从发布出来,就受到读者的好评和关注,因此,我编写了一个机器…

手机pdf文件转语音_没有电脑也能处理PDF文件,手机里的这个功能太强大!

没电脑怎么处理PDF文件?急,在线等!PDF文件因为它的强大兼容性而广泛使用,一半我们都是在电脑打开,如果你刚好没办法使用电脑怎么办?一般来说,我们手机还是能直接打开进行浏览的,以在…

图文+动画讲解排序算法总结!!

戳蓝字“CSDN云计算”关注我们哦!作者 | 大数据肌肉猿责编 | 阿秃很多同学算法零基础,我都不建议他们直接去刷「剑指offer」和「LeetCode」,可以从常见的查找和排序算法开始学起,本期我先整理了常见排序算法,大家可以收…

SpringMVC项目 使用IDEA快速构建

文章目录一、使用idea构建基础项目1. 创建一个新的项目2. 选择maven项目构建架子3. 项目储存路径4. 配置maven和仓库5. 自动下载所需依赖二、初始化项目结构2.1. 创建Java和resources文件夹2.2. 给文件夹赋予目录结构层级2.3. 初始化pom.xml2.4. 初始化web.xml2.5创建配置文件2…

GDPR到底是如何影响机器学习的?

摘要: GDPR时代来临,你的机器学习模型还能训练吗?一般数据保护条例(GDPR)对数据科学产生了很大的影响。现在GDPR有99条正文条款和173篇声明(Recital),长而复杂,但是随着时…

iphone字体_iPhone 适合老人盘吗?

过年的时候,有多少人会选择给家里的老人购买一台新智能手机做为礼物呢?又有多少人选择赠送的手机是 iPhone 呢? (图片来自网络)很久以前,咆哥就看见很多有关‘ iPhone ’是否可以作为孝心机的话题&#xff…

干货 | 金融级消息队列的演进 — 蚂蚁金服的实践之路

摘要: 小蚂蚁说: 消息队列作为一个数据的集散中心,承载了越来越多的场景和数据,从最开始的 OLTP 到 OLAP,甚至再到物联网、人工智能、机器学习等场景,都有很大的想像空间。 在能力上,消息队列现…

海底光缆,到底是怎么安装和维护的?

戳蓝字“CSDN云计算”关注我们哦!作者 | 鲜枣课堂责编 | 阿秃大家应该都知道海底光缆是什么吧?没错!简单来说,就是埋在海底的光纤线缆。如今这个时代,我们每天都在上网。通过上网,我们可以随时和世界各地保…

android padding作用,android:padding和android:layout_margin的区别

简单点来说:android:padding是内边距,控件本身的内容与控件边缘的距离。android:layout_margin是外边距,控件与其他控件之间的距离。下面以具体的例子来进行解释:1、不设置边距android:layout_width"match_parent"andro…

8张图理解Java

摘要: 一图胜千言,下面图解均来自Program Creek 网站的Java教程,目前它们拥有最多的票选。如果图解没有阐明问题,那么你可以借助它的标题来一窥究竟。8张图理解Java一图胜千言,下面图解均来自Program Creek 网站的Java…

云栖大会|十年阿里云存储进化史:今天最好的表现就是明天最低的要求

戳蓝字“CSDN云计算”关注我们哦!科技创新的能力仍然是俘获市场用户的必要条件,而善于发现新需求的敏锐嗅觉以及直面挑战的魄力,更是巨头企业的绝杀武器。随着大数据、人工智能等新兴技术的发展,图像语音识别、智能机器人、自动驾…

他在阿里的逆袭,只因为想做个“锤子”

摘要: 我叫孟白,这是我在阿里的第五年。这五年,我从一个外包工程师,到同事眼中的“铁打的”技术专家。填过无数次坑,也无数次想过放弃。留在阿里,是因为同事一句无意间的玩笑:“你只能做钉子&am…