胡琪

为今天工作,为明天投资,为未来孵化一些东西

dex中类的加载流程和unexpected DEX异常

也谈安卓dex的动态加载与MultiDex和热修复中我们讲解了安卓dex文件的加载过程,那么dex文件加载到内存后类的加载流程是怎样的呢?为何被打上预校验的类与其引用的类不在同一个dex中会出现unexpected DEX异常呢?这就需要从安卓中类加载的native层代码寻找答案了。从native代码我们能够更加清楚的明白QQ空间热修复的插桩字节码的原理以及比QQ空间更优的解决办法。

安卓class文件的加载过程及unexpected DEX

前面说过加载一个类起始于ClassLoader中的loadClass方法,关键函数的调用流程如下:

ClassLoader.loadClass->DexPathList.findClass->DexFile.loadClassBinaryName->dalvik_system_DexFile.cpp中Dalvik_dalvik_system_DexFile_defineClassNative
->Class.cpp中的dvmDefineClass->findClassNoInit

可以看到在native层加载类调用的是findClassNoInit函数,这个函数非常长,我们截取其中的关键代码看下这个函数是如何加载一个类的:

可以看到findClassNoInit首先会从缓存的已加载了的类的HashTable中去查找该类是否已经加载过,如果已经加载过直接返回该类,否则会从dex中查找该类,如果找到会将其转换为ClassObject对象(对应java层的Class),然后将其添加到HashTable中进行缓存,然后会调用dvmLinkClass函数链接该类,而在链接的过程中调用了Resolve.cpp中的dvmResolveClass函数来解析class,在这个函数中会出现QQ空间热修复提到的预校验问题,因为这个函数比较重要而且函数也不长,因此看下其完整的代码:

可以从源代码的if语句中提炼出导致抛出unexpected DEX异常需要同时满足以下3个条件(其实是4个条件,只不过因为resClassCheck->classLoader != NULL是恒成立的,所以只能考虑其余3个条件),如下图所示

《dex中类的加载流程和unexpected DEX异常》

可以看到只有当3个条件同时满足的时候才会抛出unexpected DEX异常,出现QQ空间热修复中提到的pre-verified问题。换而言之,只要我们破坏这三个条件中的某一个即可防止该异常抛出,我们来看下这三个条件成立的情况:

  1. !fromUnverifiedConstant,即fromUnverifiedConstant==false,而这个fromUnverifiedConstant即是调用dvmResolveClass函数时传入的一个bool参数
  2. IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED),加载的类的flag是CLASS_ISPREVERIFIED
  3. referrer->pDvmDex != resClassCheck->pDvmDex,加载的类和被引用类不在同一个dex中

再来看看如何破坏这3个条件,一个一个来看:

!fromUnverifiedConstant

也就是说我们要破坏这个条件需要让fromUnverifiedConstant==true。而这个值是作为参数传入的,那怎样让这个参数传入的时候传入true呢?我们在函数的注释中看到了如下一段注释:

从注释可以看到当加载的类被调用的方式是”const-class” or “instance-of”指令的时候该值会被设置为true,这里所说的指令指的是dalvik的指令,那么哪些引用方式会触发”const-class” or “instance-of”呢?最基本的引用一个类的常量引用会触发dalvik的const-class指令,如

大家可以写一个简单的java文件,然后反编译为smaili文件看下就知道了。而使用instanceof运算符会触发instance-of指令。那么要破坏这个条件就很容易了只需要在首次加载了包含被引用类(也就是热修复中的补丁类)的dex之后,主动使用一些能够触发补丁类”const-class” or “instance-of”指令的语句即可,如

或者

其中fixbug.class为原dex和补丁dex中重复的类,即被某个类引用的补丁类,注意使用该方法需要让多个dex中重复的类(即被某个类引用的补丁类,也就是需要修复的类)最先加载。也就是说要让触发这2个指令的操作放在使用这些类之前,因此应该将此操作和加载多个dex一样放在Application类中完成,推荐在加载了补丁dex之后就触发这些补丁类的这2个指令操作。

IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)

要破坏这个条件需要让引用类(引用补丁类的类)的flag不是CLASS_ISPREVERIFIED,也就是QQ空间热修复提到的pre-verified问题,这个QQ空间已经给出了解决方案,即通过插桩的方式来防止类被打上该标识来解决,大家可以看QQ空间的热修复的那篇文章详细了解,【QQ空间技术团队】安卓App热补丁动态修复技术介绍,这里还是提一下,引用类被打上了CLASS_ISPREVERIFIED标识,说明在原始dex中该类所有的directMethods方法和virtualMethods方法中直接引用到的所有的类(第一层级关系,不会递归搜索)都和该类在同一个dex中,这点可以从注释中看出来:

注意打上CLASS_ISPREVERIFIED标识的是引用类,不是被引用的补丁类,另外打上该标识的操作是在dexopt阶段完成的而不是类加载阶段

referrer->pDvmDex != resClassCheck->pDvmDex

要破坏这个条件即需要让被引用的类referrer(即补丁类)和引用类resClassCheck在同一个dex中,也就是热修复中的补丁类和所有的直接引用它的类都在同一个dex中,目前tinker的热修复方案在热修复原理上采用的是和QQ空间一样的理论,只不过在处理预校验问题上的解决方式不同,tinker在规避预校验问题采用的是将补丁类和原始dex合成为全新的dex,然后将这个新dex放到Elements数组最前面优先加载,这样打上预校验标志的类即使恰好引用了该补丁类,它们也仍然在同一个dex中了。因此破坏这种条件的方案直接参考tinker即可。

总结

解决某个被打上CLASS_ISPREVERIFIED预校验标识导致unExpected异常包含3种解决思路:

  1. 以QQ空间为代表的防止类被打上该标志,通过字节码插桩方式让原dex中的每个类引用一个与其不在同一个dex中的某个类,这样原dex中的每一个类都不会被打上CLASS_ISPREVERIFIED标志
  2. 以Tinker为代表的让打上该标志的类和其引用的所有的类都在同一个dex中,具体做法是让补丁类与原dex进行合成为新的dex,这样即使某个被打上CLASS_ISPREVERIFIED标志的类引用了该补丁类,它们也仍然在同一个dex中
  3. 通过让补丁类触发”const-class” / “instance-of”指令来达到让fromUnverifiedConstant标志为true的目的,从而避免预校验过程

3种方案的比较

首先QQ空间这种方式是不大推荐的,因为QQ空间实际上是破坏了执行dexopt阶段时类被打上CLASS_ISPREVERIFIED标识,而dalvik虚拟机之所以要在dexopt阶段就将这些类打上该标识是为了提高运行效率,但是如果在dexopt阶段阻止某些本应该被打上CLASS_ISPREVERIFIED/CLASS_ISOPTIMIZED标志的类被打上该标识, 那么这些类的Verify和Optimize都将在类的初始化阶段进行,很显然在类的初始化阶段去执行这些操作会降低运行效率,因为dexopt操作是在apk安装时进行的,这会将verify和optimize从安装时改到了运行时进行,用图表示如下:

《dex中类的加载流程和unexpected DEX异常》

图片引用自https://yq.aliyun.com/articles/222892?utm_content=m_32258#

另外也可以看到QQ空间的插桩做法实际上是对未出现bug前的原dex进行处理,因为无法预计哪个类可能会出现bug,所以需要对原dex中的每个类都插入一段引用另外一个单独dex中类的代码片段。因此最坏的情况就是无出现bug的类,此时插桩就失去了意义还会白白损耗性能。

而Tinker以及方案3属于对出现bug之后的处理,Tinker将出现bug的类修复后与原dex进行合成,方案3通过让出现bug的类修复后主动触发”const-class” / “instance-of”指令。

从操作过程来看,QQ空间方案属于预防,在发布的原包dex的每个类中插入一段引用另外一个单独dex中类的代码片段从而达到每个类都不被打上CLASS_ISOPTIMIZED标志,以避免出现bug后热修复时预校验问题。属于未雨绸缪,不管下不下雨,随时带把伞使得万一下雨时有伞可用。Tinker和方案3属于补救,Tinker出现bug修复后,让修复的类和原dex合成为全新的dex,不规避打上CLASS_ISOPTIMIZED,而是避免打上了该标志的类与其直接引用的所有的类不在同一个dex中这一问题。方案3通过让补丁类主动触发”const-class” / “instance-of”指令避免补丁类进行预校验。很显然出现bug后补救的方案更好,因为预防在很多情况下是不必要的,就像可能绝大多数情况下都不会下雨,比如前面说过最坏的情况就是无bug类出现此时插桩只会白白损耗性能。而补救是在出现bug之后,已经明确知道出现bug了。

 

打赏

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注