0%

第4章-资源热修复技术

普遍的实现方式

目前市面上很多资源热修复都参考了 Instant Run 的实现,首先看下 Instant Run 是怎么做到资源热修复的:

  1. 创建一个新的 AssetManager (AssetManager.class.getConstructor().newInstance()),并通过反射调用 addAssetPath 添加 sdcard 上的新资源包

  2. 反射所有 Activity 中 AssetManager 的引用处,全部换成刚才新建的 newAssetManager

  3. 得到 Resource 的弱引用集合,把它们的 AssetManager 成员替换成 newAssetManager

总体来说就是2步:构造一个新的 AssetManager ,并通过反射调用 addAssetPath ,这样就得到一个含有所有新资源的 AssetManager;找到所有引用到原有 AssetManager 的地方,通过反射把引用处替换成 newAssetManager 。

资源文件的格式

我们随便找个带资源的APK,用 AAPT 解析以下,可以看到内容大概是这样的:

aapt d resources App-debug.apk

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

这就表示,activity_main.xml 这个资源编号是 0x7f040019, 其中packageid 是 0x7f , 资源类型id 是 0x04 ,在Type String Pool 中对应的正是 layout 类型,而 0x04 类型的 第 0x0019 个资源就是 activity_main 这个资源。

运行时资源的解析

默认由 Android SDK 编出来的 APK 是由 AAPT 工具打包的,其资源包的 package id 就是 0x7f 。在走到 App第一行代码之前,系统就已经帮我们构建好一个已经添加了安装包资源的 AssetManager 了,即包含了 package id 为0x01 的 framework-res.jar 中资源和 package id 为 0x7f 的 App 安装包资源

如果补丁包中资源的 package id 也是 0x7f ,就会使得同一个 package id 的包被加载2次,怎么解决呢?

在Android L 之后这是没问题的,因为它会默默把后来的包添加到之前的包的同一个 PackageGroup 下面,仍旧会加入到该类型的 TypeList 中,只是会打出一个 warning log。但是,使用时获取某个 Type 资源时,会从前往后遍历,也就是说先得到原有安装包里的资源,补丁包中的资源永远无法生效了。所以,在Android L 以上的版本,在原有的 AssetManager 上加入补丁包,是没有任何作用的。

而在 Android 4.4 以下版本,addAssetPath 只是把补丁包的路径添加到了 mAssetPath 中,这时候早已经错过真正解析资源包时间了。

以上解释了为什么像 Instant Run 这种方案,一定需要一个全新的 AssetManager ,然后再加入完整的新资源包,替换原有的 AssetManager。

另辟蹊径的资源修复方案

一个好的资源修复方案,首先补丁包要足够小,直接下发完整的补丁包肯定是不行的。目前主要有以下方案:

  • 对资源包做差量处理,在运行时合成完整包,虽然减少了包体积,但是运行时的合成操作耗费了运行事件和内存。

  • 自己修改 AAPT ,在打补丁包时对资源重新编号,这样会涉及修改 Android SDK 工具包,不利于集成,也无法很好地对将来的 AAPT 升级。

我们的方案:构造一个package id 为 id 0x66 的资源包,它只包含改变了的资源项,直接在原有 AssetManager 中 addAssetPath 这个包即可。补丁包的资源,只包含原有包没有而新报里面有的新增资源,以及发生了改变的资源。对于增加、减少、修改这3种情况,我们要如何处理呢?

  • 新增资源直接假如补丁包

  • 减少资源,只要不使用这个资源就好了

  • 修改资源,比如替换了一张图片,那么将其视为新增资源,在打入补丁包的时候,代码引用处也做相应修改,把原来使用的旧资源id的地方变为新的 id

一张图说明下这些情况(绿线表示新增资源,红线表示发生修改的资源,黑线表示内容没有变化,但是id发生了变化的,x表示删除了的资源):

资源补丁包构建

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

新资源插入的位置是随机的,这与每次 AAPT 打包时解析 XML 的顺序有关。

所以,新增的资源导致它们所属的 type 中跟在它们之后的资源 id 发生了位移,发生位移的资源不会加入补丁包中,但是在补丁包的代码中会调整 id 的引用处,如下所示:

1
imageView.setImageResource(R.drawable.holo)

R.drawable.holo 是一个int 值,它的值是 AAPT 指定的,可以用反编译工具可以看到它的真实值:

1
imageView.setImageResource(0x7f020002)

打出一个新包时,对于开发者而言,holo 的图片内容没变,代码引用处也没变,但是在新包里面,同样这句话,由于新资源插入导致 id 改变,所以引用实际变成了:

1
imageView.setImageResource(0x7f020003)

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

1
imageView.setImageResource(0x7f020002)

内容发生改变的资源

内容发生改变,比如 activity_main.xml 文件内容改变了,也可能我们修改了string 类型的值,他们都会加入到补丁包中,并重新编号,相应代码也会改变:

1
setContentView(R.layout.activity_main)

实际上就是:

1
setContentView(0x7f030000)

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

1
setContentView(0x66020000)

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

删除了的资源

不影响补丁包,就不多言

对于type的影响

上面说的所谓简单,值得是运行时应用补丁变得简单了。真正复杂的地方在于构造补丁。

更优雅地替换 AssetManager

分2种情况:

  • 对于 Android L 以后的版本,直接在原有 AssetManager 上应用补丁就行了,并且由于是应用原来的 AssetManager ,与 Instant Run 方案比,省略了大量的反射和替换操作,提升了加载补丁的效率

  • 之前提过,在Android KK 和以下版本,addAssetPath 是不会加载资源的。我们对原有的 AssetManager 先进行析构,再重构的时候将补丁包资源也加入,用的还是原来的 AssetManager,同样避免了反射和替换操作(Instant Run 方案还是必须重新构造一个新的 AssetManager 并加入补丁包,再替换原来的)

意料之外的资源问题

在加载完补丁之后,如果做了 new WebView() 操作,就会发现找不到新资源的问题,这是因为 WebView 初始化时可能会构造新的 ResourceImpl ,替换掉了原先的 ResourceImpl ,从而把补丁资源给一起丢掉了。其他的就不展开。

小结

对比市面上的方案,我们的方案优势:

  • 不侵入打包,直接对比新旧资源即可产生补丁资源(对比修改 AAPT)

  • 不必下发完整包,补丁中只有变动的资源(对比 Instant Run、Amigo等方式)

  • 不需要在运行时合成完整包,不占用运行时的计算和内存资源(对比Tinker)

唯一需要注意的是,因为对新资源的引用是在新代码中,所有资源修复需要代码修复的支持的

可能的问题:查找旧 id 的时候,是直接对 int 值替换,所以会找到 0x7f?????? ,如果开发者也使用了 0x7f?????? 这样的数字,就会导致数字会被错误地转换。

谢谢你的鼓励