一步步手动实现热修复

来源:互联网 发布:宁波 知乎 编辑:程序博客网 时间:2024/05/29 12:05

今日科技快讯

昨日,乐视超级汽车战略合作伙伴法拉第未来(Faraday Future,简称FF)在美国拉斯维加斯发布旗下首款量产车FF 91。根据现场介绍,FF 91续航超800公里,0-60 英里/小时加速时间仅为2.39 秒,加速性能超越 Ferrari 488 GTB、TESLA Model S P100D 在内的顶级跑车和同级别豪华车。此外,发布会演示环节还通过乐视超级手机,无需人工复杂操作即可作为车辆钥匙及车主识别功能,为用户提供个性化需求及强大的交互体验。

作者简介

本篇来自 SAHADEV 的投稿,详细介绍了如何实现热修复,作者原文中有很多热修复相关博文推荐,由于篇幅原因,我这里没有贴出,感兴趣的朋友可以点击最后 阅读原文 查看。

SAHADEV 的博客地址:

http://blog.csdn.net/sahadev_

前言

热修复技术自从QQ空间团队搞出来之后便渐渐趋于成熟。这里我们主要介绍如何一步步手动实现基本的热修复功能,无需使用第三方框架。

本文示例所用到的任何资源都已开源,项目中包含工程中所用到代码、示例图片、说明文档。项目地址为:

https://code.csdn.net/u011064099/sahadevhotfix/tree/master

dex文件的生成与加载

我们在这部分主要做的流程有:

1. 编写基本的Java文件并编译为 .class 文件。

2. 将 .class 文件转为 .dex 文件。

3. 将转好的 dex文件 放入创建好的Android工程内并在启动时将其写入本地。

4. 加载解压后的 .dex 文件中的类,并调用其方法进行测试。

Note: 在阅读本节之前最好先了解一下 类加载器的双亲委派原则DexClassLoader的使用以及反射的知识点。

编写基本的Java文件并编译为.class文件

首先我们在一个工程目录下开始创建并编写我们的Java文件,你可能会选择各种IDE来做这件事,但我在这里劝你不要这么做,因为有坑在等你。等把基本流程搞清楚可以再选择更进阶的方法。这里我们可以选择文本编辑器比如EditPlus来对Java文件进行编辑。

新建一个Java文件,并命名为:ClassStudent.java,并在java文件内键入以下代码:

public class ClassStudent {
   private String name;
   
   public ClassStudent() {}
   
   public void setName(String name) {
       this.name = name;    }
       
   public String getName(){
       return this.name + ".Mr";      }}

Note: 这里要注意,不要对类添加包名,因为在后期对class文件处理时会遇到问题,具体问题会稍后说明。上面的 getName 方法在返回时对 this.name 属性添加了一段字符串,这里请注意,后面会用到。

在文件创建好之后,对Java文件进行编译:

将.class文件转为.dex文件

好,现在我们使用class文件生成对应的dex文件。生成dex文件所需要的工具为dx,dx工具位于sdk的 build-tools 文件夹内,如下图所示:

Tips: 为了方便使用,建议将dx的路径添加到环境变量中。如果对dx工具不熟悉的,可以在终端中输入 dx –help 以获取帮助。

dx工具的基本用法是:

dx --dex [--output=<file>] [<file>.class | <file>.{zip,jar,apk} | <directory>]

Tips: 刚开始自己摸索的时候,就没有仔细看命令,导致后面两个参数的顺序颠倒了,搞出了一些让人疑惑难解的问题,最后又不得不去找dx工具的源码调试,最后才发现自己的问题在哪。如果有对dx工具感兴趣的,可以对dx的包进行反编译或者获取dx的相关源代码进行了解。dx.lib文件位于dx.bat的下级目录lib文件夹中,可以使用JD-GUI工具对其进行查看或导出。如果需要获取源代码的,请使用以下命令进行克隆:

Git clone https://android.googlesource.com/platform/dalvik

我们使用以下命令生成dex文件:

dx --dex --output=user.dex ClassStudent.class

这里我为了防止出错,提前在当前目录下新建好了 user.dex 文件。上述命令依赖编译.class文件的JDK版本,如果使用的是JDK8编译的class会提示以下问题:

PARSE ERROR:unsupported class file version 52.0...while parsing ClassStudent.class1 error; aborting

这里的52.0意味着class文件不被支持,需要使用JDK8以下的版本进行编译,但是dx所需的环境还是需要为JDK8的,这里我编译class文件使用的是JDK7,请注意。

上面我们提到了为什么先不要在ClassStudent中使用包名,因为在执行dx的时候会报以下异常,这是因为以下第二项条件没有通过,该代码位于 com.Android.dx.cf.direct.DirectClassFile 文件内:


运行截图如下所示:

好了,到此为止我们的目录应该如下:

写入dex到本地磁盘

接下来将生成好的user.dex文件放入Android工程的res\raw文件夹下:

在系统启动时将其写入到磁盘,这里不再贴出具体的写入代码,项目的MainActivity中包含了此部分代码。

加载dex中的类并测试

在写入完毕之后使用DexClassLoader对其进行加载。DexClassLoader 的构造方法需要4个参数,这里对这4个参数进行简要说明:

  • String dexPath:dex文件的绝对路径。在这里我将其放入了应用的cache文件夹下。

  • String optimizedDirectory:优化后的dex文件存放路径。DexClassLoader在构造完毕之后会对原有的dex文件优化并生成一个新的dex文件,在这里我选择的是 …/cache/optimizedDirectory/ 目录。此外,API文档对该目录有严格的说明:Do not cache optimized classes on external storage.出于安全考虑,请不要将优化后的dex文件放入外部存储器中。

  • String libraryPath:dex文件所需要的库文件路径。这里没有依赖,使用空字符串代替。

  • ClassLoader parent:双亲委派原则中提到的父类加载器。这里我们使用默认的加载器,通过getClassLoader()方法获得。

在解释完毕 DexClassLoader 的构造参数之后,我们开始对刚刚的dex文件进行加载:


接来下开始load我们刚刚写入在dex文件中的 ClassStudent 类:

Class<?> aClass = dexClassLoader.loadClass("ClassStudent");

然后我们对其进行初始化,并调用相关的 get/set 方法对其进行验证,在这里我传给 ClassStudent 对象一个字符串,然后调用它的get方法获取在方法内合并后的字符串:

Object instance = aClass.newInstance();
Method method = aClass.getMethod("setName", String.class);method.invoke(instance, "Sahadev");
Method getNameMethod = aClass.getMethod("getName");
Object invoke = getNameMethod.invoke(instance);

最后我们实现的代码可能是这样的:


最后附上我们的运行截图:

类的加载机制简要介绍

本节内容是为了给下节内容做知识铺垫,所以如果要需要了解热修复技术,本节内容的知识点必不可少。

一个类在被加载到内存之前要经过加载、验证、准备等过程。经过这些过程之后,虚拟机才会从方法区将代表类的运行时数据结构转换为内存中的Class。

我们这节内容的重点在于一个类是如何被加载的,所以我们从类的加载入口开始。

类的加载是由虚拟机触发的,类的加载入口位于 ClassLoader loadClassInternal() 方法:


这段方法还有段注释说明:这个方法由虚拟机调用用来加载一个类。我们看到这个类的内部最后调用了 loadClass() 方法。那我们进入 loadClass() 方法看看:

public Class<?> loadClass(String name) throws ClassNotFoundException {
   return loadClass(name, false);}

loadClass() 方法方法内部调用了 loadClass() 的重载方法:


loadClass() 方法大概做了以下工作:

  • 首先查找该类是否已经被加载.

  • 如果该ClassLoader有父加载器,那么调用父加载器的loadClass()方法.

  • 如果没有父加载器,则调用 findBootstrapClassOrNull() 方法进行加载,该方法会使用引导类加载器进行加载。普通类是不会被该加载器加载到的,所以这里一般返回null.

  • 如果前面的步骤都没找到,那调用自身的findClass()方法进行查找。

好,ClassLoader 的 findClass() 方法是个空方法,所以这个过程一般是由子加载器实现的。Java的加载器这么设计是有一定的渊源的,感兴趣的读者可以自行查找书籍了解。

protected Class<?> findClass(String name) throws ClassNotFoundException {
   throw new ClassNotFoundException(name);}

在Android中,ClassLoader 的直接子类是 BaseDexClassLoader,我们看一下 BaseDexClassLoader 的 findClass() 实现:


Tips: 有需要虚拟机以及类加载器全套代码的,请使用以下命令克隆:

Git clone https://android.googlesource.com/platform/dalvik-snapshot

相关代码位于项目的 ics-mr1 分支上。

看到这里我们可以知道,Android中类的查找是通过这个 pathList 进行查找的,而 pathList 又是个什么鬼呢?

在 BaseDexClassLoader 中声明了以下变量:

/** structured lists of path elements */
private final DexPathList pathList;

所以我们可以看看 DexPathList findClass() 方法做了什么:


这里通过遍历 dexElements 中的 Element对象 进行查找,最终走的是 DexFile 的 loadClassBinaryName() 方法:


到此为止,我们就将类的加载过程梳理完了。

Class文件的替换

本节主要依赖文章:

http://blog.csdn.net/vurtne_ye/article/details/39666381

中的未实现代码实现,实现思路也源自该文章,在阅读本文之前可以先行了解。

这一节我们主要实现的流程有:

  • 在工程内创建相同的ClassStudent类,但在调用 getName() 方法返回字符串时会稍有区别,用于结果验证

  • 使用 DexClassLoader 加载外部的 user.dex

  • 将 DexClassLoader 中的 dexElements 放在 PathClassLoader 的 dexElements 之前

  • 验证替换结果

创建工程内的ClassStudent

上面演示了如何加载外部的Class,为了起到热修复效果,那么我们需要在工程内有一个被替换的类,替换的ClassStudent类内容如下:


外部的ClassStudent类的内容如下:


这两个类除了在 getName() 方法返回之处有差别之外,其它地方一模一样,不过这足可以让我们说明情况。

我们这里要实现的目的: 我们默认调用getName()方法返回的是“xxxx.Miss”,如果热修复成功,那么再使用该方法的话,返回的则会是“xxxx.Mr”

对含有包名的类再次编译

因为第一节中专门声明了不可以对类声明包名,但是这样在Android工程中无法引用到该类,所以把不能声明包名的问题解决了一下。

不能声明包名的主要原因是在编译Java文件时,没有正确的使用命令。对含有包名的Java文件应当使用以下命令:

javac -d ./ ClassStudent.java

经过上面命令编译后的.class文件便可以顺利通过dx工具的转换。

我们还是按照第一节的步骤将转换后的user.dex文件放入工程中并写入本地磁盘,以便稍后使用。

替换工程内的类文件

在开始之前还是再回顾一下实现思路(详见上一大章节):类在使用之前必须要经过加载器的加载才能够使用,在加载类时会调用自身的 findClass() 方法进行查找。然而在Android中类的查找使用的是 BaseDexClassLoader,BaseDexClassLoader 对 findClass() 方法进行了重写。

我们可以得知类的查找是通过遍历 dexElements 来进行查找的。所以为了实现替换效果,我们需要将 DexClassLoader 中的 Element对象 放到 dexElements数组第0个位置,这样才能在 BaseDexClassLoader 查找类时先找到 DexClassLoader 所用的 user.dex 中的类。

类的加载是从上而下加载的,所以就算是 DexClassLoader 加载了外部的类,但是在系统使用类的时候还是会先在 ClassLoader 中查找,如果找不到则会在 BaseDexClassLoader 中查找,如果再找不到,就会进入 PathClassLoader 中查找,最后才会使用 DexClassLoader 进行查找,所以按照这个流程外部类是无法正常发挥作用的。所以我们的目的就是在查找工程内的类之前,先让加载器去外部的dex中查找。

好了,再次梳理了思路之后,我们接下来对思路进行实践。

下面的方法是我们主要的注入方法:


这段代码的核心在于将 DexClassLoader 中的 dexElements PathClassLoader 中的 dexElements 进行合并,然后将合并后的 dexElements 替换 原先的dexElements。最后我们在使用 ClassStudent类 的时候便可以直接使用外部的ClassStudent,而不会再加载默认的ClassStudent类。

首先我们通过 classLoader 获取各自的 pathList 对象:


在使用以上反射的时候要注意,pathList属性 属于 基类BaseDexClassLoader。所以如果直接获取 DexClassLoader 或者 PathClassLoader的pathList 属性的话,会得到null。

其次是获取 pathList 对应的 dexElements,这里要注意 dexElements 是个 数组对象


接下来我们将两个数组对象合并成为一个:


上面这段代码我们根据数组对象的类型创建了一个新的大小为2的新数组,并将两个数组的第一个元素取出,将代表 外部dex 的 dexElement 放在了第0个位置。这样便可以确保在查找类时优先从外部的dex中查找。

最后将原先的dexElements覆盖


验证替换结果

好,我们做完以上的工作之后,写一段代码来进行验证:


如果我们没有替换成功的话,那么这里默认使用的是内部的ClassStudent,getName()返回的会是Lavon.Miss

如果我们替换成功的话,那么这里默认使用的是外部的ClassStudent,getName()返回的则会是Lavon.Mr

我们实际运行看下效果:

这说明我们已经完成了基本的热修复。

如果你想阅读关于热修复更多的资料,请点击下方的 阅读原文 ,到作者的博客当中去继续深入研究。

更多

每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都有好心情。

如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。

欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:

原创粉丝点击