阿里妹导读:Sophix 是阿里推出的史上首个非侵入式移动热更新解决方案,自去年推出已有一年的时间了。这一年来,阿里集团内外成千上万的app踊跃接入。由于接入简便,操作流畅,功能可靠,资源占用极小,Sophix得到了广大开发者的好评,网上也出现了大量开发者亲身实践的接入文章。

今天,我们选取了其中一个改进点——资源补丁的精简优化,来详细介绍一下 Sophix 背后的技术。

这一年,关于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。由此就可以得到这个<string name="app_name">MyDemo</string>资源的完整信息了。

这里我们可以看出,一个资源中占空间最大的正是字符串池,其他结构只是一些索引数字,所占空间很小,因此如果能对字符串池进行精简,将节省很多空间。

字符串池的构造

首先,我们得先弄清字符串池的结构是怎样的,它的关键入口是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的完整字符串池就可以作为补丁的字符串池,我们最早的资源补丁就是直接采用这种方式。这么做有一个好处,就是新增和修改的资源用到的字符串索引完全不需要修改,就可以正常获取到字符串池的具体值。但是,由于字符串池是从完整的新包中直接拿过来的,因此,里面非新增和修改的资源所用的字符串也直接包含在了其中,而这些字符串对于补丁,是多余的。因此,我们需要精简去除的,正是这些无用的字符串。

具体来说,主要分为三个步骤:

  1. 首先,我们需要确定要留下的是哪些字符串。
  2. 接着,重新编排留下的有效字符串,使其紧凑对齐,并且重新计算各个字符串相对起始位置的偏移。
  3. 最后,修正所有引用字符串的地方,使得补丁资源可以正确地引用到重排过的字符串。

确定要留下的字符串

需要留下的字符串,无疑就是补丁资源中使用的字符串,而补丁资源中使用的字符串,就是我们通过比较新包和旧包,得到的新增和修改的资源所用到的字符串。具体来说,我们已经通过比较得到了一个映射表,里面记录了所有新包资源到补丁资源的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热修复技术原理》免费电子书,详细讲解了代码、资源、动态库的热修复实现(在阿里技术公众号,回复“热修复”,即可下载)。值此一周年之际,我们与电子工业出版社合作,计划在近期出版该书的印刷纸质版,并新增了一些篇章,以方便大家翻阅,敬请期待。

最后,手淘基础平台部EMAS平台诚招Android高级开发工程师/专家,欢迎各位优秀靠谱的小伙伴加入,

查看职位详情:https://job.alibaba.com/zhaopin/position_detail.htm?positionId=46817

或者发送简历至xiaolin.gxl@alibaba-inc.com。


每天一篇技术文章,

看不过瘾?

关注“阿里巴巴机器智能”微信公众号

发现更多AI干货。

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

  1. 最强神作 Crysis深度剖析与优化指南

    题目:最强神作!Crysis深度剖析与优化指南 作者:小熊在线--WolStame 介绍:最强游戏Crysis全方位剖析与深度优化指南 关键:CRYSIS/DX10游戏/技术剖析/优化 原创:小熊在线 ...

  2. 最强神作!Crysis深度剖析与优化指南

    题目:最强神作!Crysis深度剖析与优化指南 作者:小熊在线--WolStame 介绍:最强游戏Crysis全方位剖析与深度优化指南 关键:CRYSIS/DX10游戏/技术剖析/优化 原创:小熊在线 ...

  3. 最强神作!Crysis深度剖析与优化指南(1-8)

    题目:最强神作!Crysis深度剖析与优化指南 作者:小熊在线--WolStame 介绍:最强游戏Crysis全方位剖析与深度优化指南 关键:CRYSIS/DX10游戏/技术剖析/优化 原创:小熊在线 ...

  4. 深度剖析 Linux 的 3 种“拷贝”命令

    作者 | 奇伢       责编 | 欧阳姝黎 概述 Linux 下有 3 种"拷贝",分别是 ln,cp,mv,这 3 个命令貌似都能 copy 出一个新的文件出来. 细心的小伙 ...

  5. 深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!

    ‍‍‍‍‍‍‍‍‍‍‍‍阅读本文大约需要 20 分钟. 大家好,我是 Kaito. 这篇文章我想和你聊一聊,关于 Redis 分布式锁的「安全性」问题. Redis 分布式锁的话题,很多文章已经写烂了 ...

  6. 彻底搞透视觉三维重建:原理剖析、代码讲解、及优化改进

    视觉三维重建 = 定位定姿 + 稠密重建 + surface reconstruction +纹理贴图.三维重建技术是计算机视觉的重要技术之一,基于视觉的三维重建技术通过深度数据获取.预处理.点云配准 ...

  7. Mysql binlog应用场景与原理深度剖析

    1 基于binlog的主从复制 Mysql 5.0以后,支持通过binary log(二进制日志)以支持主从复制.复制允许将来自一个MySQL数据库服务器(master) 的数据复制到一个或多个其他M ...

  8. k3note Android8,联想乐檬K3 Note官方稳定版 最新VIBE刷机包 精简优化 完美加入Root权限...

    联联想乐檬K3 Note官方稳定版 最新VIBE刷机包 精简优化 完美加入Root权限 ROM介绍: 1.基于官方最新发布的VIBEUI_V3.1_1622_5.440.1_ST_K50-T5稳定版固 ...

  9. SQL Server性能调优之执行计划深度剖析 第二节 执行计划第一次实践

    SQL Server性能调优之执行计划深度剖析 第二节 执行计划第一次实践 前言:自从上一篇文章发出之后,收到了很朋友的关注.很多朋友要求多多实践,而不是纯粹的理论.确实,从打算出这个系列开始,我就本 ...

最新文章

  1. QT程序启动加载流程简介
  2. SVN库迁移整理方法总结
  3. 哈尔滨__伏尔加庄园
  4. iframe标签 父子页面传值
  5. create-react-app 使用代理做 mock
  6. 采用我国国产处理器的超级计算机是,“中国芯”超级电脑合肥诞生 首次采用国产CPU芯片...
  7. MSP430程序库二UART异步串口
  8. linux diff 远程文件,登录diff命令,以单独的文件输出在linux
  9. python3廖雪峰云-python3基础教程廖雪峰云_Python GUI库大汇总
  10. linux系统编码启动,Linux启动流程介绍
  11. 科技公司如何占据了你的每一分钟?
  12. pythonflask开发web网页编辑_总结Python+Flask+MysqL的web建设技术过程
  13. c语言利用参数方程绘图,CG实验1-利用C语言图形函数绘图概要1.doc
  14. Angular.js学习笔记(1)
  15. 2021-06-17 compareAndSet 比较交换 CAS
  16. 一些压力测试结果(Mysql,Zookeeper,Redis,Mongodb)
  17. leetcode 868. Binary Gap
  18. Fireworks 激活序列码--网上找的 不定时无法使用
  19. Manjaro/Arch 软件配置安装
  20. Flutter AspectRatio 宽高比设定

热门文章

  1. 我给这个Python库打101分!
  2. 这届产品经理也太太太太难了吧!
  3. 北京那些年轻人的秘密,都藏在这篇文章里
  4. python中re模块_python中re模块的使用以及一些正则表达式的总结
  5. 19-爬虫之scrapy框架大文件下载06
  6. html弹窗中的layer,ModalLayer弹窗控件(原创)
  7. js数组中的引用类型
  8. (扫盲)RPC远程过程调用
  9. 2017广西邀请赛重现赛
  10. 一文读懂 HTTP/2 特性