Nuwa学习笔记

https://github.com/jasonross/Nuwa

Nuwa is a goddess in ancient Chinese mythology best known for repairing the pillar of heaven.
女娲是中国古代神话女神以补天而文明。

With this Nuwa project,you can also have the repairing power, fix your android applicaiton without have to publish a new APK to the appstore.
简单描述:其实就是Android热修复(ps:这里不做热修复的比对,纯粹的学习笔记)
使用方法,github上描述的很清楚

详细请参考Nuwa 
懒得看英文的童鞋请看这里基于Nuwa实现Android自动化HotFix

一.原理分析
首先我来先需要了解以下大Google的分包方案,multidex 不了解的童鞋请移步MultiDex安装过程源码分析
multidex安装过程总结

将/data/app/apkName.apk路径下解压得到的classes2.dex, …, classesN.dex,依次写入到/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes2.zip等zip文件的classes.dex中,并返回这个zip列表。然后针对这个zip列表执行安装过程,具体过程是,将这个要安装的zip列表加入BaseDexClassLoader的pathList实例的dexElements数组中,其中会针对各dex文件进行dex2opt优化。一旦加入到了dexElements数组中,程序启动的时候,ClassLoader会加载dexElements数组中的元素,从而实现multi dex的安装。

其实简要的概括就是把多个dex文件塞入到app的classloader之中,但是android dex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢?
让我们来看看类加载的代码:

public Class findClass(String name, List suppressed) {  
    for (Element element : dexElements) {  //每个Element就是一个dex文件
        DexFile dex = element.dexFile; 
                 if (dex != null) { 
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); 
            if (clazz != null) { 
                return clazz;
                            }
               }
   }               
 if (dexElementsSuppressedExceptions != null) {      
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    } 
    return null;
}

一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:

Nuwa学习笔记_第1张图片

在此基础上,我们构想了热补丁的方案,把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图


Nuwa学习笔记_第2张图片

1.动态加载补丁dex,并将补丁dex插入到dexElements最前面
2.要实现热更新,需要热更新的类要防止被打上ISPREVERIFIED标记,关于这个标记,请阅读QQ空间的解决方案

二源码探究
对于第一点,通过DexClassLoader对象,将补丁dex对象加载进来,再通过反射将补丁dex插入到dexElements最前面即可。(可以照搬multidex源码)
而对于第二点,关键就是如何防止类被打上ISPREVERIFIED这个标记。

问题产生:
试验一下,修改某个类,然后打包成dex,插入到classloader,当加载类的时候出现了(本例中是ActivityManager要被替换):


Nuwa学习笔记_第3张图片

为什么会出现以上问题呢?
从log的意思上来讲,ModuleManager引用了ActivityManager,但是发现这这两个类所在的dex不在一起,其中:
1. ModuleManager在classes.dex中
2. ActivityManager在patch.dex中
结果发生了错误。
这里有个问题,拆分dex的很多类都不是在同一个dex内的,怎么没有问题?
让我们搜索一下抛出错误的代码所在,嘿咻嘿咻,找到了一下代码:

Nuwa学习笔记_第4张图片

从代码上来看,如果两个相关联的类在不同的dex中就会报错,但是拆分dex没有报错这是为什么,原来这个校验的前提是:


如果引用者(也就是ModuleManager)这个类被打上了 CLASS_ISPREVERIFIED标志
,那么就会进行dex的校验。那么这个标志是什么时候被打上去的?

让我们在继续搜索一下代码,嘿咻嘿咻~~,在DexPrepare.cpp找到了一下代码:
Nuwa学习笔记_第5张图片

这段代码是dex转化成odex(dexopt)的代码中的一段,我们知道当一个apk在安装的时候,apk中的classes.dex会被虚拟机(dexopt)优化成odex文件,然后才会拿去执行.
虚拟机在启动的时候,会有许多的启动参数,其中一项就是verify选项,当verify选项被打开的时候,上面doVerify变量为true,那么就会执行dvmVerifyClass进行类的校验,如果dvmVerifyClass校验类成功,那么这个类会被打上CLASS_ISPREVERIFIED的标志,那么具体的校验过程是什么样子的呢?
此代码在DexVerify.cpp中,如下:

Nuwa学习笔记_第6张图片
  1. 验证clazz->directMethods方法,directMethods包含了以下方法:

  2. static方法

  3. private方法

  4. 构造函数

  5. clazz->virtualMethods

  6. 虚函数=override方法?

概括一下就是如果以上方法中直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED标志


Nuwa学习笔记_第7张图片

所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED标志

最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下:

if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}

Nuwa学习笔记_第8张图片

其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了。只要没被打上这个标志的类都可以进行打补丁操作。
然后在应用启动的时候加载进来.AntilazyLoad类所在的dex包必须被先加载进来,不然AntilazyLoad类会被标记为不存在,即使后续加载了hack.dex包,那么他也是不存在的,这样屏幕就会出现茫茫多的类AntilazyLoad找不到的log。
所以Application作为应用的入口不能插入这段代码。(因为载入hack.dex的代码是在Application中onCreate中执行的,如果在Application的构造函数里面插入了这段代码,那么就是在hack.dex加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志)。
其中:
class ClassVerifier {
public static boolean PREVENT_VERIFY = false;//false防止代码被执行,提高性能
}
之所以选择构造函数是因为他不增加方法数,一个类即使没有显式的构造函数,也会有一个隐式的默认构造函数。
空间使用的是在字节码插入代码,而不是源代码插入,使用的是javaassist库来进行字节码插入的。
隐 患

虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提高性能的,我们强制防止类被打上标志是否会影响性能?这里我们会做一下更加详细的性能测试。
但是在大项目中拆分dex的问题已经比较严重,很多类都没有被打上这个标志。

如何打包补丁包:

1.空间在正式版本发布的时候,会生成一份缓存文件,里面记录了所有class文件的md5。还有一份mapping混淆文件。
2.在后续的版本中使用-applymapping选项,应用正式版本的mapping文件,然后计算编译完成后的class文件的md5和正式版本进行比较,把不相同的class文件打包成补丁包。
备注:该方案现在也应用到我们的编译过程当中,编译不需要重新打包dex,只需要把修改过的类的class文件打包成patch dex,然后放到sdcard下,那么就会让改变的代码生效。


Android历练记 是一个关于Android最新技术探讨,包含安全,架构,Android技术开发,ui绘制,源码解析等领域,如果你有兴趣,我们可以一起讨论学习,
关注微信公众号 Android历练记 或扫一扫二维码:

Nuwa学习笔记_第9张图片
Android历练记

你可能感兴趣的:(Nuwa学习笔记)