十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
这篇文章主要介绍“Android资源热修复技术怎么应用”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Android资源热修复技术怎么应用”文章能帮助大家解决问题。
企业建站必须是能够以充分展现企业形象为主要目的,是企业文化与产品对外扩展宣传的重要窗口,一个合格的网站不仅仅能为公司带来巨大的互联网上的收集和信息发布平台,创新互联面向各种领域:户外休闲椅等成都网站设计、营销型网站解决方案、网站设计等建站排名服务。目前市面上的很多资源热修复方案基本上都是参考了 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 表示删除了的资源。
可以看到,新的资源包与旧资源包相比,新增了 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);
然后再进行后续代码的对比。这样后续代码对比时就不会检测到发生了改变。
而对于内容发生改变的资源(类型为 layout 的 activity_main,这可能是我们修 改了 activity_main.xml 的文件内容。还有类型为 string 的 no,可能是我们修改了这个字符串的值),它们都会被加入到 patch 中,并重新编号为新 id。而相应的代码,也会发生改变,比如,
setContentView(R.layout.activity_main);
实际上也就是
setContentView(0x7f030000);
在生成对比新旧代码之前,我们会把新包里面的这行代码变为
setContentView(0x6 6020000);
这样,在进行代码对比时,会使得这行代码所在函数被检测到发生了改变。于是相应的代码修复会在运行时发生,这样就引用到了正确的新内容资源。
对于删除的资源,不会影响补丁包。
这很好理解,既然资源被删除了,就说明新的代码中也不会用到它,那资源放在那里没人用,就相当于不存在了。
可以看到,由于 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和系统加载资源的代码。
对于 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这一步,也可以采用我们这种方式进行替换,省时省力又省心。
关于“Android资源热修复技术怎么应用”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注创新互联行业资讯频道,小编每天都会为大家更新不同的知识点。