深入理解Android热修复技术原理之资源热修复技术

 更新时间:2021年6月29日 00:01  点击:1284

一、普遍的实现方式

目前市面上的很多资源热修复方案基本上都是参考了 Instant Run的实现。

简要说来,Instant Run中的资源热修复分为两步:

1.构造一个新的 AssetManager,并通过反射调用 addAssetPath,把这个完 整的新资源包加入到AssetManager中。这样就得到了一个含有所有新资源的 AssetManager。

2.找到所有之前引用到原有 AssetManager的地方,通过反射,把引用处替换 为 AssetManager。

一个 Android 进程只包含一个 ResTable, ResTable 的成员变量 mPackageGroups 就是所有解析过的资源包的集合。任何一个资源包中都含有 resources.arsc,它记录了所有资源的id分配情况以及资源中的所有字符串。这些信息是以二进制方式存储的。底层的AssetManager做的事就是解析这个文件,然后把相关信息存储到 mPackageGroups 里面。

二、资源文件的格式

整个 resources.arse 文件,实际上是由一个个 ResChunk (以下简称 chunk) 拼接起来的。从文件头开始,每个 chunk 的头部都是一个 ResChunk_header结构,它指示了这个chunk的大小和数据类型。

通过ResChunk_header中的type成员,可以知道这个chunk是什么类型, 从而就可以知道应该如何解析这个chunko

解析完一个 chunk 后,从这个 chunk + size的位置开始,就可以得到下一个 chunk 起始位置,这样就可以依次读取完整个文件的数据内容。

一般来说,一个 resources.arsc 里面包含若干个package,不过默认情况下, 由打包工具aapt 打出来的包只有一个 package。这个 package里包含了 app中的 所有资源信息。

资源信息主要是指每个资源的名称以及它对应的编号。我们知道,Android中的每个资源,都有它唯一的编号。编号是一个 32 位数字,用十六进制来表示就是0xPPTTEEEE。PP 为 package id, TT 为 type id, EEEE 为 entry id。

它们代表什么?在 resources.arse 里是以怎样的方式记录的呢?

  • 对于 package id,每个 package 对应的是类型为 RES_TABLE_PACKAG E_ TYPE 的 ResTable_package 结构体,ResTable_package 结构体的 id 成员变量就表示它的 package id。
  • 对于 type id,每个type对应的是类型为 RES_TABLE_TYPE_SPEC_ TYPE 的 ResTable_typeSpec 结构体。它的id成员变量就是type id。但是,该type id 具体对应什么类型,是需要到package chunk 里的 Type String Pool 中去解析得到的。比如 Type String Pool 中依次有 attr、 drawablex mipmap、layout 字符串。就表示 attr 类型的 type id 为 1, drawable 类型的 type id 为 2, mipmap 类型的 type id 为 3, layout 类型的type id 为 4。所以,每个 type id对应了 Type String Pool里的字符顺序 所指定的类型。
  • 对于 entry id,每个 entry表示一个资源项,资源项是按照排列的先后顺序 自动被标机编号的。也就是说,一个type里按位置出现的第一个资源项,其 entry id 为0x0000,第二个为 0x0001,以此类推。因此我们是无法直接指定entry id的,只能够根据排布顺序决定。资源项之间是紧密排布的,没有空隙,但是可以指定资源项为ResTable_type::NO_ENTRY来填入一个空资源。

举个例子,我们随便找个带资源的 apk,用 aapt解析一下,看到其中的一行是:

$ aapt d resources app-debug.apk

 ......

 spec resource 0x7f040019 com.taobao.patch.demo:layout/activity_main: flags=0x00000000

 ......

这就表示,activity_main.xml 这个资源的编号是 0x7f040019。它的 package id 是 0x7f,资源类型的id为0x04, Type String Pool里的第四个字符串正是 layout 类型,而 0x04 类型的第 0x0019 个资源项就是 activity_main 这个资源。

三、运行时资源的解析

默认由 Android SDK 编出来的 apk,是由 aapt 具进行打包的,其资源包的 package id 就是 0x7f。

系统的资源包,也就是 framework-res.jar, package id 为 0x01。

在走到 app的第一行代码之前,系统就已经帮我们构造好一个已经添加了安装包资源的 AssetManager 了。

因此,这个 AssetManager里就已经包含了系统资源包以及 app的安装包,就是 package id 为 0x01 的 framework-res.jar 中的资源和 package id 为 0x7f 的 app 安装包资源。

如果此时直接在原有 AssetManager 上继续 addAssetPath的完整补丁包的 话,由于补丁包里面的package id 也是 0x7f,就会使得同一个 package id的包被 加载两次。这会有怎样的问题呢?

在 Android L 之后,这是没问题的,他会默默地把后来的包添加到之前的包的同—个 PackageGroup 下面。

而在解析的时候,会与之前的包比较同一个 type id所对应的类型,如果该类型 下的资源项数目和之前添加过的不一致,会打出一条warning log,但是仍旧加入到该类型的TypeList 中。

在获取某个 Type的资源时,会从前往后遍历,也就是说先得到原有安装包里 的资源,除非后面的资源的config比前面的更详细才会发生覆盖。而对于同一个 config 而言,补丁中的资源就永远无法生效了。所以在 Android L以上的版本,在原有AssetManager 上加入补丁包,是没有任何作用的,补丁中的资源无法生效。

而在 Android 4.4 及以下版本,addAssetPath只是把补丁包的路径添加到 了 mAssetPath中,而真正解析的资源包的逻辑是在app第一次执行 AssetManager::getResTable 的时候。

而在执行到加载补丁代码的时候,getResTable已经执行过了无数次了。这是因为就算我们之前没做过任何资源相关操作,Android framework里的代码也会多 次调用到那里。所以,以后即使是addAssetPath,也只是添加到了 mAssetPath, 并不会发生解析。所以补丁包里面的资源是完全不生效的!

所以,像 Instant Run 这种方案,一定需要一个全新的 AssetManager时,然后再加入完整的新资源包,替换掉原有的AssetManager。

四、另辟蹊径的资源修复方案

而一个好的资源热修复方案是怎样的呢?

首先,补丁包要足够小,像直接下发完整的补丁包肯定是不行的,很占用空间。

而像有些方案,是先进行 bsdiff,对资源包做差量,然后下发差量包,在运行时 合成完整包再加载。这样确实减小了包的体积,但是却在运行时多了合成的操作,耗费了运行时间和内存。合成后的包也是完整的包,仍旧会占用磁盘空间。

而如果不采用类似 Instant Run 的方案,市面上许多实现,是自己修改aapt, 在打包时将补丁包资源进行重新编号。这样就会涉及到修改 Android SDK工具包, 即不利于集成也无法很好地对将来的aapt 版本进行升级。

针对以上几个问题,一个好的资源热修复方案,既要保证补丁包足够小,不在 运行时占用很多资源,又要不侵入打包流程。我们提出了一个目前市面上未曾实现 的方案。

简单来说,我们构造了一个 package id 为 0x66的资源包,这个包里只包含改变了的资源项,然后直接在原有AssetManager 中 addAssetPath 这个包。然后就可以了。真的这么简单?

没错!由于补丁包的 package id 为 0x66,不与目前已经加载的 0x7f冲突,因 此直接加入到已有的AssetManager中就可以直接使用了。补丁包里面的资源,只包含原有包里面没有而新的包里面有的新增资源,以及原有内容发生了改变的资源。

而资源的改变包含增加、减少' 修改这三种情况,我们分别是如何处理的呢?

  • 对于新增资源,直接加入补丁包,然后新代码里直接引用就可以了,没什么好说的。
  • 对于减少资源,我们只要不使用它就行了,因此不用考虑这种情况,它也不影响补丁包。
  • 对于修改资源,比如替换了一张图片之类的情况。我们把它视为新增资源, 在打入补丁的时候,代码在引用处也会做相应修改,也就是直接把原来使用旧资源 id 的地方变为新 id。

用一张图来说明补丁包的情况,是这样的:

图中绿线表示新增资源。红线表示内容发生修改的资源。黑线表示内容没有变 化,但是id 发生改变的资源。x 表示删除了的资源。

4.1、新增的资源及其导致 id 偏移

可以看到,新的资源包与旧资源包相比,新增了 holo_grey 和 dropdn_item2 资源,新增的资源被加入到 patch中。并分配了 0x66 开头的资源 id。

而新增的两个资源导致了在它们所属的 type 中跟在它们之后的资源 id发生了 位移。比如 holojight, id 由 0x7f020002 变为 0x7f020003,而 abc_dialog 由 0x7f030004 变为 0x7f030003。新资源插入的位置是随机的,这与每次 aapt打包 时解析xml 的顺序有关。发生位移的资源不会加入 patch,但是在 patch的代码中会调整id 的引用处。

比如说在代码里,我们是这么写的

imageView.setImageResource(R.drawable.holo_light);

这个 R.drawable.holojight 是一个int 值,它的值是 aapt指定的,对于开发者 透明,即使点进去,也会直接跳到对应res/drawable/holo_light.jpg,无法查看。不过可以用反编译工具,看到它的真实值是0x7f020002。所以这行代码其实等价于:

imageView.setImageResource(0x7f020002);

而当打出了一个新包后,对开发者而言,holojight的图片内容没变,代码引用处也没变。但是新包里面,同样是这句话,由于新资源的插入导致的id改变,对于 R.drawable.holojight 的引用已经变成了:

imageView.setImageResource(0x7f020003);

但实际上这种情况并不属于资源改变,更不属于代码的改变,所以我们在对比新旧代码之前,会把新包里面的这行代码修正回原来的id。

imageView.setImageResource(0x7f020002);

然后再进行后续代码的对比。这样后续代码对比时就不会检测到发生了改变。

4.2、内容发生改变的资源

而对于内容发生改变的资源(类型为 layout 的 activity_main,这可能是我们修 改了 activity_main.xml 的文件内容。还有类型为 string 的 no,可能是我们修改了这个字符串的值),它们都会被加入到 patch 中,并重新编号为新 id。而相应的代码,也会发生改变,比如,

setContentView(R.layout.activity_main); 

实际上也就是

setContentView(0x7f030000);

在生成对比新旧代码之前,我们会把新包里面的这行代码变为

setContentView(0x6 6020000);

这样,在进行代码对比时,会使得这行代码所在函数被检测到发生了改变。于是相应的代码修复会在运行时发生,这样就引用到了正确的新内容资源。

4.3、删除了的资源

对于删除的资源,不会影响补丁包。

这很好理解,既然资源被删除了,就说明新的代码中也不会用到它,那资源放在那里没人用,就相当于不存在了。

4.4、对于type的影响

可以看到,由于 type0x01 的所有资源项都没有变化,所以整个 type0x01资源都没有加入到patch 中。这也使得后面的 type 的 id 都往前移了一位。因此 Type String Pool 中的字符串也要进行修正,这样才能使得 0x01 的 type 指向 drawable, 而不是原来的 attr。

所以我们可以看到,所谓简单,指的是运行时应用patch变的简单了。

而真正复杂的地方在于构造 patch 。我们需要把新旧两个资源包解开,分别解析 其中的resources.arsc 文件,对比新旧的不同,并将它们重新打成带有新 package id 的新资源包。这里补丁包指定的 package id 只要不是 0x7f 和 0x01就行,可以是 任意0x7f 以下的数字,我们默认把它指定为 0x66。

构造这样的补丁资源包,需要对整个resources.arsc的结构十分了解,要对二 进制形式的一个一个chunk进行解析分类,然后再把补丁信息一个一个重新组装成 二进制的chunk。这里面很多工作与 aapt做的类似,实际上开发打包工具的时候也是参考了很多aapt和系统加载资源的代码。

五、更优雅地替换AssetManager

对于 Android L 以后的版本,直接在原有 AssetManager 上应用 patch就行 了。并且由于用的是原来的AssetManager,所以原先大量的反射修改替换操作就 完全不需要了,大大提高了加载补丁的效率。

但之前提到过,在 Android KK 和以下版本,addAssetPath是不会加载资源 的,必须重新构造一个新的AssetManager 并加入 patch,再换掉原来的。那么我们不就又要和Instant Run —样,做一大堆兼容版本和反射替换的工作了吗?

对于这种情况,我们也找到了更优雅的方式,不需要再如此地大费周章。

明显,这个是用来销毁 AssetManager并释放资源的函数,我们来看看它具体做了什么吧。

可以看到,首先,它析构了 native 层的 AssetManager,然后把 java层的 AssetManager 对 native 层的 AssetManager 的引用设为空。

native 层的 AssetManager 析构函数会析构它的所有成员,这样就会释放之前加载了的资源。

而现在,java 层的 AssetManager 已经成为了空壳。我们就可以调用它的 init 方法,对它重新进行初始化了!

这同样是个native方法,

这样,在执行 init 的时候,会在 native层创建一个没有添加过资源,并且 mResources 没有初始化的的 AssetManager。然后我们再对它进行 addAssetPath,之后由于 mResource 没有初始化过,就可以正常走到解析 mResources的逻辑,加载所有此时add进去的资源了 !

由于我们是直接对原有的 AssetManager进行析构和重构,所有原先对 AssetManager 对象的引用是没有发生改变的,这样,就不需要像 Instant Run那样进行繁琐的修改了。

顺带一提,类似 Instant Run 的完整替换资源的方案,在替换 AssetManager这一步,也可以采用我们这种方式进行替换,省时省力又省心。

六、本章小结

总结一下,相比于目前市面上的资源修复方式,我们提出的资源修复的优势在于:

  • 不侵入打包,直接对比新旧资源即可产生补丁资源包。(对比修改 aapt方式的 实现)
  • 不必下发完整包,补丁包中只包含有变动的资源。(对比 Instanat Run,Amigo 等方式的实现)
  • 不需要在运行时合成完整包。不占用运行时计算和内存资源。(对比 Tinker的 实现)

唯一有个需要注意的地方就是,因为对新的资源的引用是在新代码中,所有资源修复是需要代码修复的支持的。也因此所有资源修复方案必然是附带代码修复的。而 之前提到过,本方案在进行代码修复前,会对资源引用处进行修正。而修正就是需要 找到旧的资源id,换成新的id。查找旧 id 时是直接对 int值进行替换,所以会找到 0x7f ?????? 这样的需要替换 id。但是,如果有开发者使用到了 0x7f ??????这样的数字,而它并非资源id,可是却和需要替换的id数值相同,这就会导致这个数字 被错误地替换。

但这种情况是极为罕见的,因为很少会有人用到这样特殊的数字,并且还需要碰巧这数字和资源id相等才行。即使出现,开发者也可以用拼接的方式绕过这类数字的产生。所以基本可以不用担心这种情况,只是需要注意它的存在。

以上就是深入理解Android热修复技术原理之资源热修复技术的详细内容,更多关于Android资源热修复的资料请关注猪先飞其它相关文章!

[!--infotagslink--]

相关文章

  • Android子控件超出父控件的范围显示出来方法

    下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
  • Android开发中findViewById()函数用法与简化

    findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20
  • Android模拟器上模拟来电和短信配置

    如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
  • 夜神android模拟器设置代理的方法

    夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
  • android自定义动态设置Button样式【很常用】

    为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
  • Android WebView加载html5页面实例教程

    如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
  • 深入理解Android中View和ViewGroup

    深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
  • Android自定义WebView网络视频播放控件例子

    下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
  • Android用MemoryFile文件类读写进行性能优化

    java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
  • Android设置TextView竖着显示实例

    TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
  • android.os.BinderProxy cannot be cast to com解决办法

    本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20
  • Android 实现钉钉自动打卡功能

    这篇文章主要介绍了Android 实现钉钉自动打卡功能的步骤,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下...2021-03-15
  • Android 开发之布局细节对比:RTL模式

    下面我们来看一篇关于Android 开发之布局细节对比:RTL模式 ,希望这篇文章对各位同学会带来帮助,具体的细节如下介绍。 前言 讲真,好久没写博客了,2016都过了一半了,赶紧...2016-10-02
  • Android中使用SDcard进行文件的读取方法

    首先如果要在程序中使用sdcard进行存储,我们必须要在AndroidManifset.xml文件进行下面的权限设置: 在AndroidManifest.xml中加入访问SDCard的权限如下: <!--...2016-09-20
  • Android开发之PhoneGap打包及错误解决办法

    下面来给各位简单的介绍一下关于Android开发之PhoneGap打包及错误解决办法,希望碰到此类问题的同学可进入参考一下哦。 在我安装、配置好PhoneGap项目的所有依赖...2016-09-20
  • 用Intel HAXM给Android模拟器Emulator加速

    Android 模拟器 Emulator 速度真心不给力,, 现在我们来介绍使用 Intel HAXM 技术为 Android 模拟器加速,使模拟器运行度与真机比肩。 周末试玩了一下在Eclipse中使...2016-09-20
  • Android判断当前屏幕是全屏还是非全屏

    在安卓开发时我碰到一个问题就是需要实现全屏,但又需要我们来判断出用户是使用了全屏或非全屏了,下面我分别找了两段代码,大家可参考。 先来看一个android屏幕全屏实...2016-09-20
  • Android开发中布局中的onClick简单完成多控件时的监听的利与弊

    本文章来为各位介绍一篇关于Android开发中布局中的onClick简单完成多控件时的监听的利与弊的例子,希望这个例子能够帮助到各位朋友. 首先在一个控件加上这么一句:and...2016-09-20
  • Ubuntu 系统下安装Android开发环境 Android Studio 1.0 步骤

    Android Studio 是一个Android开发环境,基于IntelliJ IDEA. 类似 Eclipse ADT,Android Studio 提供了集成的 Android 开发工具用于开发和调试,可以在Linux,Mac OS X,Window...2016-09-20
  • Android实现简单用户注册案例

    这篇文章主要为大家详细介绍了Android实现简单用户注册案例,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-05-26