Android 热修复Nuwa的原理及Gradle插件源码解析

现在,热修复的具体实现方案开源的也有很多,原理也大同小异,本篇文章以Nuwa为例,深入剖析。
Nuwa的github地址
https://github.com/jasonross/Nuwa
以及用于hotpatch生成的gradle插件地址
https://github.com/jasonross/NuwaGradle

而Nuwa的具体实现是根据QQ空间的热修复方案来实现的。安卓App热补丁动态修复技术介绍。在阅读本篇文章之前,请先阅读该文章。

从QQ空间终端开发团队的文章中可以总结出要进行热更新只需要满足下面两点就可以了:

  • 动态加载补丁dex,并将补丁dex插入到dexElements最前面
  • 要实现热更新,需要热更新的类要防止被打上ISPREVERIFIED标记,关于这个标记,请阅读上面QQ空间团队的文章。

对于第一点,实现很简单,通过DexClassLoader对象,将补丁dex对象加载进来,再通过反射将补丁dex插入到dexElements最前面即可。具体可参考谷歌的Multidex的实现。

而对于第二点,关键就是如何防止类被打上ISPREVERIFIED这个标记。

简单来说,就是将所有类的构造函数中,引用另一个hack.dex中的类,这个类叫Hack.class,然后在加载补丁patch.dex前动态加载这个hack.dex,但是有一个类的构造函数中不能引用Hack.class,这个类就是Application类的子类,一旦这个类的构造函数中加入Hack.class这个类,那么程序运行时就会找不到Hack.class这个类,因为还没有被加载。也就是说,一个类直接引用到的类不在同一个dex中即可。这样,就能防止类被打上ISPREVERIFIED标记并能进行热更新。

我们先来看Nuwa的实现,再去看Nuwa的插件的实现。

使用Nuwa的时候需要在attachBaseContext方法中初始化

这里写图片描述

Nuwa预先将Hack.class这个类(空实现)打成apk文件,放在asserts目录中,在init方法中,做的就是将asserts目录中的这个文件拷贝到文件目录下。

首先创建文件目录将asserts目录下的hack.apk拷到该目录,然后调用loadPatch方法将该apk动态加载进来。loadPatch方法也是之后进行热修复的关键方法,你的所有补丁文件都是通过这个方法动态加载进来。

loadPatch方法中主要是调用DexUtils.injectDexAtFirst()方法将dex插入到dexElements最前面。该方法如下。

根据传入的dex的文件目录defaultDexOptPath,构造DexClassLoader对象dexClassLoader,然后通过getDexElements方法获得原来的dexElements对象,之后拿到dexClassLoader对象中的dexElements对象,调用combineArray方法将这两个对象进行结合,将我们传进来的dex插到该对象的最前面,之后调用ReflectionUtils.setField()方法,将dexElements进行替换。combineArray方法中做的就是扩展数组,将第二个数组插入到第一个数组的最前面

之后如果你有补丁要应用,直接调用Nuwa.loadPatch()方法,传入补丁的目录,重启应用之后就可以进行热更新了。这是Nuwa应用层的实现,可以看到,并不复杂。相对复杂的是Gradle插件层的实现。Gradle插件要做的事就是拿到所有class,在其构造函数中注入Hack.class,使其直接引用另一个dex中的文件,防止被打上ISPREVERIFIED标记。并且混淆的时候要应用上一次release版本的mapping文件。现在有两点关键内容:

  • 如何拿到所有的class
  • 如何在构造函数中注入代码

我们先来解决第二点,如何注入代码,Nuwa使用的是asm注入代码。

现在假设我们已经存在了hack.apk,并且里面已经有了Hack.class文件,其源代码如下

我们编写一个测试类Test,里面有一个测试方法,我们需要将Hack.class注入到Test的构造函数中,让其直接引用另一个dex中的类。

我们编译一下,得到Test.clss,将其复制到一个目录dir。然后终端进入到该目录,使用javap命令查看字节码

这里写图片描述

可以看到图中有 字样,该处就是构造函数,然后看到4:return,这是构造函数的结束的地方。现在我们读入该文件,并对其进行字节码修改,然后写入该目录下dest目录下。在这之前,需要加入asm的依赖,至于asm的使用,请自行查询。

我们先将该文件读入,获得输入流,调用referHackWhenInit方法,将输入流传入,用ClassVisitor对象访问该对象,实现MethodVisitor方法,在该方法中访问对象中的方法,对方法名进行判断,如果是构造函数,则对其进行字节码注入操作,接下来运行main方法,查看dest目录下生成的文件。

生成的Test.class文件内容如下

这里写图片描述

可以看到构造函数中直接引用了Hack.class,然后我们使用javap命令查看字节码

这里写图片描述

可以看到return之前,插入了我们的字节码,直接引用了Hack.class

字节码注入的问题解决了,接下来就是找到要注入字节码的所有class。

接下来分析Nuwa的Gradle插件,在分析之前,请先了解一下Gralde插件的开发流程,可以阅读这篇文章如何使用Android Studio开发Gradle插件。下面的内容的gradle版本是基于1.2.3,高版本的可能有所差异。请查看项目依赖的是否是下面的这个版本

为了找到这些class,实际上,分为了两种情况

  • 开启了Multidex的项目
  • 没有开启Multidex的项目

如果使用了MultiDex,并且没有混淆,这种情况很简单,dex任务之前会生成一个jar文件,包含了所有的class,所以做起来很容易。但是如果添加了混淆怎么办?试了一下,也是proguard后也是生成了一个jar包,也没啥问题

为了验证作者的论证,我们编写一个插件来验证一下,关于如何编写插件,请查看上面贴的文章。

我们先在项目中开启Multidex

对于release的构建,开启混淆,对于debug,关闭混淆

这个插件的作用是什么的,其实很简单,就是输出preDex,dex,proguard这三个Task的输入文件,当然前提是Task存在。代码如下

应用插件然后查看插件输出的日志

这里写图片描述

可以看到,对于debug的构建,我们没有开启混淆,dex的Task的输入文件是一个allclasses.jar,而release版本的构建,dex的Task的输入文件是混淆之后的文件classes.jar。并且无论是debug还是release,对于这种开启了Multidex的情况下,是不存在preDex这个Task的,对于这种情况,我们可以判断preDex这个Task是否存在进行操作。查看NuwaGradle的源码。相关解释,我已经加入到注释中了。

而上面使用到了nuwaPrepareClosure和copyMappingClosure这两个闭包。以及Gradle插件的初始化操作如下,详情见注释

而对于没有开启Multidex的情况,则会存在一个preDex的Task。preDex会在dex任务之前把所有的库工程和第三方jar包提前打成dex,下次运行只需重新dex被修改的库,以此节省时间。dex任务会把preDex生成的dex文件和主工程中的class文件一起生成class.dex,这样就需要针对有无preDex,做不同的修改字节码策略即可。源码解释如下。

这样就完成了字节码的修改,至于字节码修改的函数,其实就和最开始的测试asm修改字节码的例子差不多,对于jar文件,需要将jar文件中的所有class遍历一遍处理。字节码的注入操作全在NuwaProcessor这个类中。源码解析如下

字节码的注入需要将Application类排除在外,这个类如果在Manifest文件中设置了,我们需要将其拿到,并加入到excludeClass中,这个操作在nuwaPrepareClosure闭包中已经处理了。

然后如果开启了混淆,我们需要应用上一次发release版本的mapping文件进行混淆

Nuwa除了支持某个构建执行打Patch的操作之外,还支持批量生产所有构建的Patch,该Task的名字为nuwaPatches,这个Task的依赖关系还要处理一下

所有需要注入字节码的类处理完毕后,我们需要将其进行dex操作,使其能够运行在Android系统上。这个方法在NuwaAndroidUtils类中。

还有一个hash值的工具类,包括从文件中解析hash值到map,将hash值格式化写入文件,判断hash值知否一样。

以及一个文件操作的工具类

最后,总结一下NuwaGradle的流程。

  • 首先判断preDex这个Task是否存在
  • 如果不存在,则对dex的输入文件进行遍历,这些输入文件是一系列的jar,对这些jar进行判断,看其是否满足注入字节码的条件,如果满足,对jar文件中满足条件的class文件进行遍历注入字节码,然后删除原来的jar,将处理后的文件命名为原来的文件。
  • 如果存在这个preDex,将这个preDexTask的输入文件进行字节码注入操作,这个Task的输入文件是一系列的jar文件,这些jar是所有的库工程和第三方jar包,此外,还需要将主工程的class文件进行处理。
  • 完成了注入字节码操作后,需要对其进行dex操作,也就是最终的patch文件。这个patch文件可以直接被客户端加载并进行热修复。
  • 不能注入字节码的类是Application的子类,因为Hack.apk在程序运行之前没有被加载,所以如果Application类中引用了Hack.apk中的Hack.class文件,则会报Class找不到的异常,之后也永远找不到了。所以这个类不能注入字节码,但是需要提前加载初始化方法中动态加载该Hack.apk。
  • 发版时的mapping文件以及所有class文件的hash值的文件需要保持下来打patch使用。

Gradle可以做很多自动化的东西

Gradle可以做很多自动化的东西

Gradle可以做很多自动化的东西

重要的事说三遍

1 1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部