本文章主要根据阿里出的《深入探索Android热修复技术原理》后的个人总结

一、为什么直接补丁类直接导入到补丁包中,运行类加载时会产生异常并退出?

首先,因为dex加载到本地内存时,如果不存在odex文件,那么首先会执行dexopt,其中

if(doVerify){

if(dvmVerifyClass(class)){

((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;

verified = true;

}

}

dvmVerifyClass方法如果APK只存在一个dex文件,直接返回true,这时类会被打上CLASS_ISPREVERIFIED标记。

在运行时,由于补丁类跟原类不在同一个dex文件中,此时原类已经被打上了ISPREVERIFIED标记,由于此时在不同的dex文件了,就会直接抛出dvmThrowIllegalAccessError异常。

解决方案:

创建一个新的无关的帮助类放到一个单独的dex文件中,原dex中所有的构造函数都引用这个类,一般方案是侵入dex打包流程,利用.class字节码修改技术,在所有.class文件的构造函数中引用这个类,插桩由此而来。这样做在dexopt时,类将不会被加上CLASS_ISPREVERIFIED标志。因此运行时就不会有这个异常了。

但是如此的话,本来只需要DEX第一次加载到内存时校验一次即可,但是因为未加CLASS_ISPREVERIFIEDQ标记,那么类的校验和优化都将在类的初始化阶段进行。

虽然每次加载加一个类时间的是很快的,但是每次APP启动的时候会加载很多类,就会造成短暂白屏,这对用户来说无法容忍。


Dalvik虚拟机加载一个类,有三部ResolveClass,LinkClass,InitClass。

Link:父类或实现接口权限控制检查主要发生在Link阶段。

Init:这个方法主要完成父类的初始化、当前类的初始化、静态变量的初始化赋值等操作。


QFix方案:

手Q热修复方案巧妙的躲过了插桩方案,躲过原理如下:

首先类加载时由于调用类已经打上了CLASS_ISPREVERIFIED标记,所以会被检查,检查时又由于不在同一个DEX文件而直接报错,所以这时如果能让两个类的pDvmDex相同,那么就可以绕过这个判断进行demDexSetResolvedClass()将类放入到虚拟机。

如果将补丁类放到原有的dex的pRexClasses数组中?

1、具体实现,首先通过补丁工具反编译dex为smali文件拿到对应文件:

preResolveClz:需要打包的补丁类(类A)的描述符,非必需,为了调试方便加上的这个参数。

refererClz:需要打包的补丁类(类A)所在的dex文件的任何一个类描述符。只要是与该补丁类在同一个DEX文件中即可,所以拿了DEX文件中的第一个类。

classIdx:需要打包的类A在原有dex文件中的类索引ID,如2425

2、通过dlopen拿到libdvm.so库的句柄,通过dlsym拿到so库的dvmResolveClass/dvmFindLoadedClass函数指针。

3、预加载引用类:android/support/annotation/AnimRes,这个AnimRes就是之前说的refererClz即当前dex文件的第一个类,这样dvmFindLoadedClass(“android/support/annotation/AnimRes”)返回值才不为null,然后dvmFindLoadedClass执行结果ClassObject作为第一个参数执行dvmResolveClass(AnimRes,2425,true)即可。

第三个参数fromUnverifiedConstance,如果是true直接绕过检查,直接将类加载到虚拟机中。

4、真正载入类时,由于之前已经把类加载到了虚拟机中,所以下一次在虚拟机中获取resClass时就会直接返回class对象而不再进行判断。

5、JNI实现方案就是获取到原调用类的对象,然后原类的id,直接调用dvmResolveClass就可以了。

*其中AnimRes,2425都是原DEX中的。书本第83页有源代码

这样其实就绕过了加载时的dex判断,但是这个有个天然的缺陷就是,由于是在dexopt后绕过的,dexopt会改变原有的很多逻辑,许多odex层面的优化会固定字段和方法的访问偏移,这就会导致比较严重的BUG。


以上两个方法都是为了绕过是否在同一个Dex的判断,而这个进行校验的判断条件就是:

if(!fromUnverifiedConstance && IS_CLASS_FLAG_SET(referrer,CLASS_ISPREVERIFIED))

插装法是为了给第二个条件变为false,手Q是给第一个条件变为false。

但是手Q仅仅是把第三个参数变成true然后改变第一个条件变成false的话,还是没有把类加载到虚拟机中,下一次也就是真正加载类的时候还是获取不到类,导致还是要失败,所以还需要所需要dex指针及类对应dex中的id,进行加载。

建议看一下书的第83页。


Dalvik虚拟机加载dex文件,只会加载classes.dex文件,之外的dex文件会被直接忽略。所以补丁类需要跟现有dex合并为一个classes.dex文件。

Art虚拟机则会先加载classes.dex文件,然后依次加载其他的dex文件,并且后续出现在其他dex文件中的“补丁类”将不再会被加载。所以Art下的方案:把最新的补丁类dex命名为classes.dex,原dex依次命名为classes(2、3、4…).dex就可以了,然后一起打包为一个压缩文件。再通过DexFile.loadDex得到DexFile对象,最后用该DexFile对象整体替换旧的dexElements数组就可以了。


DexFile.loadDex尝试把一个DEX文件加载到内存中,在加载到native内存之前,如果dex不存在对应的odex,那么Dalvik虚拟机会进行dexopt,Art虚拟机会进行dexoat,最后得到的都是一个优化后的odex。实际上最后虚拟机执行的是这个odex而不是dex。

因为我们执行的是odex文件,需要编译并加载dex到内存,对于dalvik虚拟机来说,因为最后加载的是一个合成完的dex文件,所以影响不大,因为总是要加载的。但是对于Art虚拟机来说,问题就很大,因为新的loadDex是补丁dex和Apk中原dex合并成一个完整补丁压缩包,所以dexoat会非常耗时。所以如果优化后的odex文件没生成或者不完整,那么loadDex便不能在应用启动的时候进行,因为会阻塞loadDex线程,一般是主线程。为了解决这个加载慢的问题,Sophix提供的方案是:把loadDex当做一个事务,如果中途被打断,那么就删除odex文件,重启的时候如果发现存在odex文件,loadDex完之后,反射注入/替换dexElements数组,实现打包。如果不存在odex文件,那么重启另一个子线程loadDex,重启后生效。

  • 在Dalvik下用Sophix自行研发的全量dex方案。

  • 在Art下本质上虚拟机已经支持多dex的加载,所以做的仅仅是把补丁dex作为主dex(classes.dex)加载而已


QFix 多态问题

(避免插桩的方案,将新类提前加载到旧类pRexClasses中,并且用旧类的相关信息绕过检查)

A b = new B();执行会尝试加载类B,方法调用链dvmResolveClass -> dvmLinkClass -> createVtable,此时为类B创建一个Vtable,其实在虚拟机中加载每个类都会为这个类生成一个Vtable表,vtable表就是当前类的所有virtual方法的一个数组,当前类和所有继承父类的public/protect/default方法就是virtual方法,因为public/protect/default修饰的方法都是可以被继承的。private/static方法不在这个范畴,因为不能被继承。

子类vtable的大小等于子类vtable方法数+父类vtable的大小。

createVtable主要进行了一下操作:

1、判断是否有父类,如果有则将父类的vtable给子类的vtable

2、遍历子类方法,判断是否方法名和参数都一致,如果一致证明是子类已经重写父类方法,那么替换vtable对应索引,子类方法覆盖掉vtable中父类的方法

3、如果方法原型不一致则直接将方法插入到vtable的末尾

4、如果不存在父类,则直接将在子类方法到vtable

但是对于静态方法或者字段来说,b.name输出的是A方法也就是父类方法的name字段的内容,这就是因为 field/static是从当前引用类型而不是实际类型中去查找,如果找不到,再去父类中递归查找。

问题出现原因:

在optimize(dvmopt)阶段,会优化方法执行,会将virtual方法优化为OP_INVOKE_VIRTUAL_QUICK指令去执行而不是INVOKE_VIRTUAL,这个指令后面的立即数就是方法对应的索引。invoke-virtual-quick比invoke-virtual效率更高,直接从实际类型的vtable中获取方法索引,而省略了dvmResolveMethod从变量的引用类型获取该方法在vtable索引ID的步骤,所以效率更高。

然而由于绕过了检查,在打包前类A的vtable值是vtable[0]=a_t2,而由于新增了a_ti方法,类A中vtable的值变成vtable[0]=a_t1,但是obj.a_t2()这行代码在odex中的指令其实是invoke-virtual-quick A.vtable[0],所以打包前用的是a_t2打包后用的是a_t1,这就导致了方法错乱。这都是多个dex的情况。

这样就只能寄希望于在Dalvik虚拟机上的mergeDex方案,但是这个方案需要再dalvik虚拟机上进行,如果65535方法限制时,会造成内存爆炸,很可能导致更新失败。


总结:Dalvik:

QQ空间和手Q的热修复方案:

  • QQ空间的处理方式是在每个类中插入一个来自其他Dex的hack.class,由此让所有类都不满足在同一个dex中的条件从而无法pre-verified,因此不会打上IS_PREVERIFIED标签(缺点效率太低,每次都会去加载类,并且侵入打包流程,APP打开时可能遇到白屏)

  • Tinker的方式是合成全量的dex文件,这样所有类都在全量dex中解决,从而消除类重复而带来的冲突。(粒度太细,指令合成,性价比比较低)

  • QFix的方式是获取虚拟机底层函数,提前解析所有补丁类。以此绕过pre-verify检查。(需要获取底层虚拟机的函数也就是提前加载class到原pClassDex栈中的函数,不够稳定可靠。并且与QQ空间方案一样无法增加public函数)


Sophix的方案

目的在解析这个dex的时候找不到这个类的定义。因此,只需要移除定义的入口,对于类的具体内容不进行删除,这样可以最大限度的减少offset的修改。

verifyAndOptimizeClasses中通过dexGetClassDef获取classDef(类的定义),

dexGetClassDef则是通过pDexFile->pClassDex[idx]获取的, 其中pClassDex则是由pHeader->classDefsOff偏移处开始的,一个dex里面一共有pHeader->classesDefsSiz个类定义。因此只需要把header的从classDefsOff偏移开始的classesDefsSiz个classDef,删除其中想要替换的类名就可以了。

然后修改pHeader->classesDefsSiz为修改后的数量即可。

到此为止我的理解,最终该方案虽然是合成了dex文件,但是做法只是删除了header文件中的classDefs的类的引用,这样让机器自动去查找class2 class3 等dex,自动查找功能则是由Android原生的multi-dex的实现来实现的(multi-dex是把一个APK里面用到的所有类分到classes.dex classes2.dex classes3.dex等之中,而每个dex都只包含了部分的类定义,但单个dex也是可以加载的,因为只要把所有dex都加载进去,本dex不存在的类就可以在运行期间在其他的dex中找到)


对于Application的处理

问题,由于Application是整个APP的入口,因此在进入到替换的完成dex之前就会先通过Application代码呢,因为application只能在原dex文件中,因此在加载当前dex中的application时,未加载新的dex文件,此时application类会被打上IS_PREVERIFIED标志,当真正运行时,由于新dex中的class替换旧dex,导致不在同一个dex文件中,检验失败,抛出异常。

解决方式:

sophix:直接将Application的pre-verify标志删除掉。但最后发现这样做存在不可避免的问题,因此改成了如下所说的流程,类似于Amigo的流程,只是不通过gradle插件而是让开发者自行替换。

tinker:让用户使用TinkerApplication注册到manifest中,真正的application则是当做参数传递给TinkerApplication,这样做是将用户的代码和真正的application分离开,这样入口application事先加载并不会影响真正的application,真正的application会被TinkerApplication根据生命周期通过反射调用。真正的Application会跟其他类相同的方式加载。唯一缺点就是需要对原有Application进行一定的修改。

Amigo(美团修复方案):跟Tinker的方案类似,利用自定的gradle插件将APP的Application替换成Amigo自己的另一个Application,然后等该修复的都修复完之后,再将原Application加载回来,开发者无感知。

Sophix将Application的preVerified删除掉,类加载的时候就会在执行ResolveClass,之前说过ResolveClass会对当前类doVerify操作,会对每一个指令进行校验,加载没有加载过的类也就会调用,也就是Resolve各个使用到的类,又由于Application是入口,在Application加载前,补丁dex文件中的类都未被加载,此时Resolve的各个类都是原Dex中的。接下在当补丁加载完成后,这些已经加载的类用到新dex的类时,由于也被打了pre-verified标签所以判断不在同一个dex中就会报错。

解决该问题有两个方案:

1、让Application用到的所有非系统类都和Application位于同一个dex里,这就可以保证pre-verified标志被打上,避免进入dvmOptResolveClass,而在加载完成后,再清除pre-verified标志,是的接下来使用其他类也不会报错。

2、把Application里面除了热修复框架代码以外的其他代码都剥离开,单独提出放在一个其他类里面,这样是的Application不会直接用到过多的非系统类,这样,保证每个单独拿出来的类都和Application处于同一个dex的概率还是比较大的,如果想要更保险,Application可以采用反射的方式访问这个单独的类,这样就彻底把Application和其他的类隔绝开了。

第一种方法比较简单,因为Android官方multi-dex机制会自动将Application用到的类都打包到主dex中,所以只要把热修复初始化放在attachBaseContext前面,一般都没问题。第二种方法稍加繁琐,实在代码架构层面进行重新设计,不过可以一劳永逸的解决问题。

App的真是启动顺序:

Application.attachBaseContext -> ContentProvider.onCreate -> Application.onCreate -> Activity.onCreate

因此为了保证热更新程序运行之前尽量少的有类加载那么就需要放在App.attachBaseContext中,但是attachBaseContext可能还没有权限认证,因此有可能网络不可用,所以不能在这里进行热更新下载等。

但是即使全部初始化都正确,仍然会有问题,在最终执行的时候,大多数时候都是以oat机器码的方式执行的,这里当引用localStorageUtil的init方法时,如果是根据method id直接从dex相关数据中获取ArtMethod结构然后执行,是没问题的。

但是如果oat文件做过比较大的优化(见多态的影响),变为直接从localStorageUtil对象的所属类LocalStorageUtil里面去查找init方法,这个时候可能就有问题。这时LocalStorageUtil的类的方法结构发生变化即方法索引变化了,添加或减少了方法并对该方法有了影响,就会造成问题。

正是由于这种限制,Sophix后面引用了一种新的更稳健的初始化方式,保证初始化在单独的入口类中进行,后面再用反射的方式替换为原有的Application,采用新的初始化方式。即:SophixApplication替代入口,真正实现逻辑通过反射调用,这样加载时不会加载其他非系统类。

最后其实跟Amigo的做法相同,只是把入口类暴露给了开发者,对于开发者更透明。


总结:

Dalvik:合成一个dex,做法是将原有的dex头文件head文件中注册的需要更新的类给剔除掉,只是剔除了注册信息,让dex查找的时候找不到,与multi-dex方式类似,方法会自动去其他的dex文件中找,以实现热更新替换方案。这可以避免两个dex合成一个dex时,方法数超过65535时内存爆炸的情况,也可以正常让dex最大可能的使用dvmopt优化,并且不受多态的影响(见书99页,多态对QFix的影响)

见书本105页。

art虚拟机:直接将新的dex文件替换名字为classes.dex,原dex文件依次命名为classes2 3 4.dex,由于Art虚拟机的dex加载方式,自动实现热更新。

对于Application的实现,最终方案还是替换Application,用代理Application来桥接真正的Application,利用反射调用真正Application的声明周期。

热更新总结--冷启动热更新相关推荐

  1. native react 更新机制_react-native热更新全方位讲解

    最近在研究热更新技术,看了网上各个大佬的博客,整体流程上总是卡壳.跳了几天坑,刚刚终于把简单的热更新流程跑通,现在也正在一边学习更新,一边整理资料,在此篇博客上记录操作流程,希望我的实践能帮助各位同行 ...

  2. uniapp 热更新和整包更新

    uniapp 热更新和整包更新 版本校验接口返回 自动更新 自动下载APK并安装 弹出下载APK手动安装 参考资料 版本校验接口返回 https://192.168.1.113/public/mobi ...

  3. 【热更新】游戏热更新方案

    游戏热更新方案 热更新演化 热更新方案 [1] 进程切换 1.1 利用fork.exec切换 1.2 利用网关切换 1.3 微服务 - 进程切换注意要点 [2] 动态库替换 [3] 脚本语言热更新 热 ...

  4. Lua快速入门篇(XLua教程)(Yanlz+热更新+xLua+配置+热补丁+第三方库+API+二次开发+常见问题+示例参考)

                            <Lua热更新> ##<Lua热更新>发布说明: ++++"Lua热更新"开始了,立钻哥哥终于开始此部分的探 ...

  5. 视频教程-热更新框架设计之热更流程与热补丁视频课程-Unity3D

    热更新框架设计之热更流程与热补丁视频课程 二十多年的软件开发与教学经验IT技术布道者,资深软件工程师.具备深厚编程语言经验,在国内上市企业做项目经理.研发经理,熟悉企业大型软件运作管理过程.软件架构设 ...

  6. android热补丁作用,Android热修复之 - 阿里开源的热补丁

    这里就有一个概念那就AndFix.apatch补丁用来修复方法,接下来我们看看到底是怎么实现的. 1.2 生成apatch包 假如我们收到了用户上传的崩溃信息,我们改完需要修复的Bug,这个时候就会有 ...

  7. 热修复——Bugly让热修复变得如此简单

    一.简述 在上一篇<热修复--Tinker的集成与使用>中,根据Tinker官方Wiki集成了Tinker,但那仅仅只是本地集成,有一个重要的问题没有解决,那就是补丁从服务器下发到用户手机 ...

  8. java热加载_java热加载

    应用服务器一般都支持热部署(Hot Deployment),更新代码时把新编译的确类 替换旧的就行,后面的程序就执行新类中的代码.这也是由各种应用服务器的独 有的类加载器层次实现的.那如何在我们的程序 ...

  9. 软件更新服务之客户端更新

    软件更新服务之客户端更新 在现在的软件开发和使用中,软件的更新是很关键的一环.通过不停的更新软件,迭代,给用户带来更好的体验和更多的功能以及修复用户反馈的bug.我们在更新的软件的时候,如果每次都要用 ...

最新文章

  1. NULL、0、nullptr的区别?
  2. unity中解析excel表
  3. Android之如何实现阿拉伯版本(RTL)的recycleView的网格布局
  4. 3部世界顶级宇宙纪录片,献给对宇宙万物充满好奇的你
  5. AjaxControlToolkit AjaxFileUpload 为英文的解决办法
  6. 01.神经网络和深度学习 W2.神经网络基础
  7. 数据库简介(python 版)
  8. linux qt创建静态库,QT创建与QT无关的纯C++程序和动态/静态库
  9. java api es_ES 常用java api
  10. 如何在缺乏商业项目经验的前提下成功通过面试,兼说我如何甄别非商业项目经验...
  11. ltp︱基于ltp的无监督信息抽取模块(事件抽取/评论观点抽取)
  12. SpingMVC 注解@RequestMapping、@SuppressWarnings、@Scheduled 定时器
  13. 数据结构之栈和队列(顺序栈、链栈、循环队列)
  14. Windows下Python的安装与配置
  15. BTC多空互相蓄力 短期迎来激变
  16. Scheduled定时任务
  17. 【数学分析入门】R语言独立性检验方法
  18. 软件系统等保方案,市政项目,投标项目必须
  19. 《高难度谈话》你需要知道的高效沟通技巧
  20. nth-of-type和nth-child的区别与相关使用

热门文章

  1. 计算机主板电杆,嵌入式主板的常见故障解决办法
  2. 大数据推荐算法概念简述
  3. offsetof函数的实现
  4. html图片上的灯光,CSS3 实现灯光照射显示文字动画
  5. 获取字符串长度的几种办法
  6. 双机热备和磁盘阵列柜
  7. [渝粤教育] 山东财经大学 国际金融 参考 资料
  8. 用容斥原理计算具有有限重数的多重集合的 r-组合(附代码)
  9. CodeForces - 1008D - Pave the Parallelepiped (容斥原理+重复组合公式+状态压缩+思维)
  10. 艺考报名照的尺寸是多少?如何制作艺考报名照?