热修复原理学习(6)资源热修复技术

表示 activity_main这个资源的编号是 0x7f040019,其中package id是 0x7f,资源类型ID是0x04(即layout类型),而0x04类型的第0x0019个资源项就是activity_main这个资源。

3. 运行时资源的解析

=============================================================================

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

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

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

// ResourcesManager.java

protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {

AssetManager assets = new AssetManager();

if (key.mResDir != null) { // 1

if (assets.addAssetPath(key.mResDir) == 0) { // 2

Log.e(TAG, "failed to add asset path " + key.mResDir);

return null;

}

}

return assets;

}

注释1:if语句 mResDir,指的就是安装包APK。

注释2:用新建的AssetManager调用 addAssetPath()去解析这个APK下的资源

因此这个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上加入补丁包,是没有任何作用的,补丁中的资源无法生效。

而在Android4.4及以下的版本,addAssetPath只是把补丁包的路径添加到了 mAssetPath中,而真正解析的资源包的逻辑是在App第一次执行AssetManager::getResTbale()的时候

//Android4.4_r1 frameworks/base/libs/androidfw/AssetManager.cpp

const ResTable* AssetManager::getResTable(bool required) const

{

// mResources已经存在,直接返回不再往下走

//如果已经执行过一次完整的该方法,以后都会直接return,所以补丁包来了也解析不了。

ResTable* rt = mResources;

if (rt) {

return rt;

}

const size_t N = mAssetPaths.size();

for (size_t i=0; i<N; i++) {

//真正解析package的地方

}

return rt;

}

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

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

4. 另辟蹊径的资源修复方案

================================================================================

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

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

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

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

Sophix的方案,简单来说,是构造了一个package id为0x66的资源包,这个包里只包含改变了的资源项,直接在原有的AssetManager中的addAssetPath这个包就可以了。

真的这么简单?

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

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

  • 对于新增资源,直接加入补丁包,然后新代码里直接引用就可以了,没什么好说的

  • 对于减少资源,我们只要不使用它就行了,因此不用考虑这种情况,它也不影响补丁包

  • 对于修改资源,比如替换了一张图片之类的情况。我们把它视为新增资源,在打入补丁的时候,代码在引用处也会做相应修改,也就是直接把原来使用旧资源ID的地方变为新ID。

用下图来说明补丁包的情况:

在这里插入图片描述

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

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


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

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

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

imageView.setImageResource(R.drawable.holo_light);

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

imageView.setImageResource(0x7f020002);

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

imageView.setImageResource(0x7f020003);

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

imageView.setImageResource(0x7f020002);

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

4.2 内容发生改变的资源


而对于内容发生改变的资源(类型为layout的activity_main,这可能是我们修改了activity_main.xml的文件内容。还有类型为string的no,可能是我们修改了这个字符串的值),他们都会被加入到补丁包中,并重新编号为新ID。

而相应的代码,也会发生改变,比如:

setContentView(R.layout.activity_main);

//实际上就是下面的

setContentView(0x7f030000);

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

setContentView(0x66020000);

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

4.3 删除了的资源


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

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

4.4 对于type的影响


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

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

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

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

5. 更优雅地替换AssetManager

=======================================================================================

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

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

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

在AssetManager源码里面,有一个有趣的东西:

// AssetManager.java

private native final void destroy();

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

static void android_content_AssetManager_destroy(JNIEnv* env, jobject clazz)

{

AssetManager* am = (AssetManager*) (env->GetIntField(clazz, gAssetManagerOffsets.mObject));

if (am != null) {

delete am;

env->SetIntField(clazz, gAssetManagerOffsets, 0);

}

}

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

AssetManager::~AssetManager(void)

{

int count = android_atomic_dec(&gCount);

delete mConfig;

delete mResources;

delete[] mLocal;

delete[] mVendor;

}

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

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

public final class AssetManager {

private native final void init(boolean isSystem);

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

关于面试的充分准备

一些基础知识和理论肯定是要背的,要理解的背,用自己的语言总结一下背下来。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,我能明显感觉到国庆后多了很多高级职位,所以努力让自己成为高级工程师才是最重要的。

好了,希望对大家有所帮助。

接下来是整理的一些Android学习资料,有兴趣的朋友们可以关注下我免费领取方式

①Android开发核心知识点笔记

②对标“阿里 P7” 40W+年薪企业资深架构师成长学习路线图

③面试精品集锦汇总

④全套体系化高级架构视频

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!

最重要的。

好了,希望对大家有所帮助。

接下来是整理的一些Android学习资料,有兴趣的朋友们可以关注下我免费领取方式

①Android开发核心知识点笔记

②对标“阿里 P7” 40W+年薪企业资深架构师成长学习路线图

[外链图片转存中…(img-PYpU50sj-1712030112952)]

③面试精品集锦汇总

[外链图片转存中…(img-wtIbljED-1712030112952)]

④全套体系化高级架构视频

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!

[外链图片转存中…(img-5uHzxnGm-1712030112952)]

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值