前言
说起热修复,已经是目前Android
开发必备技能。我所了解的一种实现方式就是类加载方案,即 dex
插桩,这种思路在插件化中也会用到。除此之外,还有底层替换方案,即修改替换 ArtMethod
。采用类加载方案的主要是以腾讯系为主,包括微信的 Tinker
、饿了么的 Amigo
;采用底层替换方案主要是阿里系的 AndFix
等。今天我将围绕热修复实现原理以及常见的热修复方式来讲解热修复。
目录
热修复的应用场景
热修复就是在APP
上线以后,如果突然发现有缺陷了,如果重新走发布流程可能时间比较长,重新安装APP
用户体验也不会太好;热修复就是通过发布一个插件,使APP
运行的时候加载插件里面的代码,从而解决缺陷,并且对于用户来说是无感的(用户也可能需要重启一下APP
)。
认识Java类的加载机制(双亲委派模型)
Java
负责加载class
文件的就是类加载器(ClassLoader
),APP
启动的时候,会创建一个自己的ClassLoader
实例,我们可以通过下面的代码拿到当前的ClassLoader
ClassLoader classLoader = getClassLoader();
Log.i(TAG, "[onCreate] classLoader" + ":" + classLoader.toString());
然后我们在看一下构造函数。在ClassLoader
这个类中的 loadClass()
方法,它调用的是另一个2个参数的重载 loadClass()
方法。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
我们点进去深入看一下loadClass
这个方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
通过分析loadClass
方法,你会发现,ClassLoader
加载类的方法就是loadClass
,是通过双亲委派模型(Parents Delegation Model
)实现类的加载的。既在加载一个字节码文件时,会询问当前的classLoader
是否已经加载过此字节码文件。如果加载过,则直接返回,不再重复加载。如果没有加载过,则会询问它的Parent是否已经加载过此字节码文件,同样的,如果已经加载过,就直接返回parent
加载过的字节码文件,而如果整个继承线路上的classLoader
都没有加载过,才由child
类加载器(即,当前的子classLoader
)执行类的加载工作。整个流程大致可以归纳成如下三步
1. 加载流程
- 检查当前的
classLoader
是否已经加载琮这个class
,有则直接返回,没有则进行第2步。- 调用父
classLoader
的loadClass
() 方法,检查父classLoader
是否有加载过这个class
,有则直
接返回,没有就继续检查上上个父classLoader
,直到顶层classLoader
。- 如果所有的父
classLoader
都没有加载过这个class
,则最终由当前classLoader
调用
findClass
() 方法,去dex
文件中找出并加载这个class
。
2. 优点
而采用这种类的加载机制的优点就是如果一个类被`classLoader`继承线路上的任意一个加载器加载过,后续在整个系统的生命周期中,这个类都不会再被加载,大大提高了类的加载效率。
3. 作用
- 类加载的共享功能
一些Framework
层级的类一旦被顶层,classLoader
加载过,会缓存到内存中,以后在任何地方用到,都不会去重新加载。大大提高了效率。 - 类加载的隔离功能
不同继承线路上的classLoader
加载的类,肯定不是同一个类,这样可以避免某些开发者自己去写一
些代码冒充核心类库,来访问核心类库中可见的成员变量。如java.lang.String
在应用程序启动前就
已经被系统加载好了,如果在一个应用中能够简单的用自定义的String
类把系统中的String
类替换掉
的话,会有严重的安全问题。
Android运行流程
Android运行流程简单来讲大致可以分成如下四步:
Android
程序编译的时候,会将.java
文件编译时.class
文件- 然后将.
class
文件打包为.dex
文件 - 然后
Android
程序运行的时候,Android
的Dalvik/ART
虚拟机就加载.dex
文件 - 加载其中的.
class
文件到内存中来使用
认识Android中的classload
我们知道,Android
和Java
有很深的渊源。基于jvm
的Java
应用是通过ClassLoader
对象来加载应用中的class
的。
而Android
在Java
的基础上,对jvm
又做一层优化和封装。既采用的是dalvik
虚拟机。类文件将被打包成dex
文件。底层的虚拟机是不同的,所以它们的类加载器当然也会不同。
而常见的Android类加载器有如下四种,下面我们一一讲解这四种。
BootClassLoader
:加载Android Framework
层中的class
字节码文件(类似java
的Bootstrap
ClassLoader
)PathClassLoader
:加载已经安装到系统中的Apk
的class
字节码文件(类似java
的App
ClassLoader
)DexClassLoader
:加载制定目录的class
字节码文件(类似java
中的Custom ClassLoader
)BaseDexClassLoader
:PathClassLoader
和DexClassLoader
的父类
而我们开发的APP
一定会用到BootClassLoader
、PathClassLoader
这2个类加载器,可通过如下代码进行
验证:
override fun initView(savedInstanceState: Bundle?) {
setContentView(binding.root)
var classLoader = classLoader
if (classLoader != null) {
Log.e(TAG, "myclassLoader = $classLoader")
while (classLoader!!.parent != null) {
classLoader = classLoader.parent
Log.e(TAG, "myclassLoader = $classLoader")
}
}
}
查看一下运行的日志信息如下:
2021-09-08 16:14:49.334 10117-10117/com.bnd.andserver.sample.debug E/com.bnd.andserver.sample.MainActivity: myclassLoader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.bnd.andserver.sample.debug-1/base.apk"],nativeLibraryDirectories=[/data/app/com.bnd.andserver.sample.debug-1/lib/arm64, /data/app/com.bnd.andserver.sample.debug-1/base.apk!/lib/arm64-v8a, /system/lib64, /vendor/lib64]]]
2021-09-08 16:14:49.334 10117-10117/com.bnd.andserver.sample.debug E/com.bnd.andserver.sample.MainActivity: myclassLoader = java.lang.BootClassLoader@43d3385
通过日志和上面代码,我们可以知道,可以通过上下文拿到当前类的类加载器( PathClassLoader
),然后通过getParent
()得到父类加载器( BootClassLoader
),这是由于Android
中的类加载器和java
类加载器一样使用的
是双亲委派模型。
我们知道,Android studio
不是所有源码都可以查看的,但是,通过查看一些简单的构造函数还是可以粗滤获知他们的关系的。下面给出可以查看BaseDexClassLoader,PathClassLoader,DexClassLoader
的部分代码(并非最终源码)
DexClassLoader 代码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package dalvik.system;
import java.io.File;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
PathClassLoader 代码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package dalvik.system;
import java.io.File;
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
BaseDexClassLoader 代码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package dalvik.system;
import java.io.File;
import java.net.URL;
import java.util.Enumeration;
public class BaseDexClassLoader extends ClassLoader {
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
throw new RuntimeException("Stub!");
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new RuntimeException("Stub!");
}
protected URL findResource(String name) {
throw new RuntimeException("Stub!");
}
protected Enumeration<URL> findResources(String name) {
throw new RuntimeException("Stub!");
}
public String findLibrary(String name) {
throw new RuntimeException("Stub!");
}
protected synchronized Package getPackage(String name) {
throw new RuntimeException("Stub!");
}
public String toString() {
throw new RuntimeException("Stub!");
}
}
通过初步分析你会发现,PathClassLoader,DexClassLoader
都是继承自BaseDexClassLoader
,而BaseDexClassLoader
又是继承自ClassLoader
,下面我们就一层一层分析。
认识PathClassLoader和DexClassLoader
PathClassLoader
和DexClassLoader
的源码都是属于系统级别的,我们无法在开发工具里面查看,有兴趣的同学可以研究一下Android
的源码。这里主要介绍一下他们的使用场景
先来介绍一下这两种Classloader
在使用场景上的区别
1. 使用场景的区别
1:
PathClassLoader
:只能加载已经安装到Android
系统中的apk
文件(/data/app
目录),是
Android
默认使用的类加载器.
2:DexClassLoader
:可以加载任意目录下的dex/jar/apk/zip
文件,比PathClassLoader
更灵活,是
实现热修复的重点。
2. 代码层面的差别
DexClassLoader 代码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package dalvik.system;
import java.io.File;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
PathClassLoader 代码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package dalvik.system;
import java.io.File;
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
你会通过对比你会发现,PathClassLoader
与 DexClassLoader
都继承于 BaseDexClassLoader
。PathClassLoader
与 DexClassLoader
在构造函数中都调用了父类的构造函数,但 DexClassLoader
多
传了一个 optimizedDirectory
。
认识BaseDexClassLoader
通过观察 PathClassLoader
与 DexClassLoader
的源码我们就可以确定,真正有意义的处理逻辑肯定是在 BaseDexClassLoader
中,所以下面着重分析 BaseDexClassLoader
源码。下面是一个构造方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
参数说明:
- dexPath:要加载的程序文件(一般是dex文件,也可以是jar/apk/zip文件)所在目录。
- optimizedDirectory :dex文件的输出目录(因为在加载jar/apk/zip等压缩格式的程序文件时会
解压出其中的dex文件,该目录就是专门用于存放这些被解压出来的dex文件的。 - libraryPath :加载程序文件时需要用到的库路径。
- parent :父加载器
注意:对于一个完整App的来说,程序文件指定的就是apk包中的 classes.dex 文件;但从热修 复的角度来看,程序文件指的是补丁。因为PathClassLoader只会加载已安装包中已经的dex文件,而DexClassLoader不仅仅可以加载 dex文件,还可以加载jar、apk、zip文件中的dex。而jar、apk、zip其实就是一些压缩格式,要拿到压缩包里面的dex文件就需要解压,所以,DexClassLoader在调用父类构造函数时会指定一个解压的目录。
类加载器肯定会提供有一个方法来供外界找到它所加载到的class
,该方法就是 findClass()
,不过在
PathClassLoader
和 DexClassLoader
源码中都没有重写父类的 findClass()
方法,但它们的父类
BaseDexClassLoader
就有重写 findClass()
,所以来看看 BaseDexClassLoader
的 findClass()
方法都做了哪些操作,代码如下:
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 实质是通过pathList的对象findClass()方法来获取class
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
可以看到, BaseDexClassLoader
的 findClass()
方法实际上是通过 DexPathList
的 findClass()
方法来
获取class
的,而这个 DexPathList
对象恰好在之前的 BaseDexClassLoader
构造函数中就已经被创建
好了,里面解析了dex
文件的路径,并将解析的dex
文件都存在this.dexElements
里面。所以,下面就来看看 DexPathList
类中都做了什么。
认识Element集合之DexPathList
好了,直接先看DexPathList
构造函数:
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
…
//将解析的dex文件都存在this.dexElements里面
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions)
}
这个构造函数中,保存了当前的类加载器 definingContext
,并调用了 makeDexElements()
得到 Element
集合。
通过对splitDexPath(dexPath)
源码的追踪,发现该方法的作用其实就是将dexPath
目录下的所有程序文件转变成一个File
集合。同时,dexPath
是一个用冒号(":")作为分隔符把多个程序文件目录拼接起来的字符串,比如(如:/data/dexdir1:/data/dexdir2:…)。
接下来在分析 makeDexElements()
方法:
//解析dex文件
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<I
// 1.创建Element集合
ArrayList<Element> elements = new ArrayList<Element>();
// 2.遍历所有dex文件(也可能是jar、apk或zip文件)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
...
// 如果是dex文件
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)
} else {
zip = file;
dex = loadDexFile(file, optimizedDirectory);
}
...
// 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
// 4.将Element集合转成Element数组返回
return elements.toArray(new Element[elements.size()]);
}
通过分析DexPathList
的makeDexElements
方法,你会发现,DexPathList
的构造函数是将一个个的程序文件(可能是dex、apk、jar、zip
)封装成一个个 Element
对象,最后添加到Element
集合中。
其实,Android
的类加载器(不管是PathClassLoader
,还是DexClassLoader
,它们最后在加载文件时,都是只认dex
文件,而loadDexFile()
是加载dex
文件的核心方法,他可以可以从jar
、apk
、zip
中提取出dex
。
然后我们再回头看一下ClassLoade()
加载类的方法,就是loadClass()
,最后调用findClass
方法完成的;而DexPathList
也是重写findClass()
方法。如下:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 使用pathList对象查找name类
Class c = pathList.findClass(name, suppressedExceptions);
return c;
}
最终是调用 pathList
的findClass
方法,看一下方法如下:
public Class findClass(String name, List<Throwable> suppressed) {
// 遍历从dexPath查询到的dex和资源Element
for (Element element : dexElements) {
DexFile dex = element.dexFile;
// 如果当前的Element是dex文件元素
if (dex != null) {
// 使用DexFile.loadClassBinaryName加载类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
其实 DexPathList
的 findClass()
方法很简单,就只是对 Element
数组进行遍历,一旦找到类名与name
相同的类时,就直接返回这个 class
,找不到则返回null
。
而采用DexFile
的loadClassBinaryName()
方法来加载class
,是因为一个Element
对象对应一个dex
文件,而一个dex
文件则包含多个class
。也就是说Element
数组中存放的是一个个的dex
文件,而不是class
文件。这可以从Element
这个类的源码和dex
文件的内部结构看出。
通过如上分析,我们发现整个类加载流程就是:
1:类加载器
BaseDexClassLoader
先将dex
文件解析放到pathList
到dexElements
里面
2:加载类的时候从dexElements
里面去遍历,看哪个dex
里面有这个类就去加载,生成class
对象
所以我们可以将自己的dex
文件加载到dexElements
里面,并且放在前面,加载的时候就可以加载我们插件中的类,不会加载后面的,从而替换掉原来的class
。
热修复的原理
通过上面一系列分析,我们已经知道了热修复的实现原理:
热修复的原理就是将补丁 dex 文件放到 dexElements 数组靠前位置,这样在加载 class 时,优先找到补丁包中的 dex 文件,加载到 class 之后就不再寻找,从而原来的 apk 文件中同名的类就不会再使用,从而达到修复的目的
明白了Android类的加载机制和实现原理,接下来就是热修复的实现了。
热修复的实现
知道了原理,实现就比较简单了,就添加新的dex
对象到当前APP
的ClassLoader
对象(也就是BaseDexClassLoader
)的pathList
里面的dexElements
。要添加就要先创建,我们先使用DexClassLoader
先加载插件,然后在生成插件的dexElements
,最后再添加就好了。
当然整个过程需要使用反射来实现。除此以外,常用的两种方法是使用apk
作为插件和使用dex
文件作为插件。下面的两个实现都是对程序中的一个方法进行了修改,然后分别打了 dex
包和apk
包,程序运行起来执行的方法就是插件里面的方法而不是程序本身的方法。
dex插件
对于dex
文件作为插件,和之前说的流程完全一致,先将修改了的类进行打包成dex
包,在将dex
进行加载,插入到dexElements
集合的前面即可。而打包流程是先将.java
文件编译成.class
文件,然后使用SDK工具打包成dex
文件并发布到远程服务端,然后APP
端请求下载,下载完毕加载即可。
dex打包工具
Android SDK
给我们单独提供了dex
打包工具《d8》
(在Android 构建工具 28.0.1 及更高版本中):如下图所示:
输入字节码可以是 *.class
文件也可以是JAR、APK 或 ZIP
文件的任意组合。还可以添加 DEX
文件作为 d8
的输入,以将这些文件合并到 DEX
输出中。如下所示,cmd进入要d8文件所在目录,执行要打包命令:
d8 E:\asworkspace\MyAndServer\app\build\intermediates\classes\debug\*\*.class
到了这一步,我们已经打包好了dex
文件,下面看一下具体的实现:
//在Application中进行替换
public class MApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//dex作为插件进行加载
dexPlugin();
}
...
/**
* dex作为插件加载
*/
private void dexPlugin(){
//插件包文件
File file = new File("/sdcard/FixDexTest.dex");
if (!file.exists()) {
Log.i("MApplication", "插件包不在");
return;
}
try {
//获取到 BaseDexClassLoader 的 pathList字段
// private final DexPathList pathList;
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
//破坏封装,设置为可以调用
pathListField.setAccessible(true);
//拿到当前ClassLoader的pathList对象
Object pathListObj = pathListField.get(getClassLoader());
//获取当前ClassLoader的pathList对象的字节码文件(DexPathList )
Class<?> dexPathListClass = pathListObj.getClass();
//拿到DexPathList 的 dexElements字段
// private final Element[] dexElements;
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
//破坏封装,设置为可以调用
dexElementsField.setAccessible(true);
//使用插件创建 ClassLoader
DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
//拿到插件的DexClassLoader 的 pathList对象
Object newPathListObj = pathListField.get(pathClassLoader);
//拿到插件的pathList对象的 dexElements变量
Object newDexElementsObj = dexElementsField.get(newPathListObj);
//拿到当前的pathList对象的 dexElements变量
Object dexElementsObj=dexElementsField.get(pathListObj);
int oldLength = Array.getLength(dexElementsObj);
int newLength = Array.getLength(newDexElementsObj);
//创建一个dexElements对象
Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), oldLength + newLength);
//先添加新的dex添加到dexElement
for (int i = 0; i < newLength; i++) {
Array.set(concatDexElementsObject, i, Array.get(newDexElementsObj, i));
}
//再添加之前的dex添加到dexElement
for (int i = 0; i < oldLength; i++) {
Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObj, i));
}
//将组建出来的对象设置给 当前ClassLoader的pathList对象
dexElementsField.set(pathListObj, concatDexElementsObject);
} catch (Exception e) {
e.printStackTrace();
}
}
apk插件
apk
作为插件,就是我们重新打了一个新的apk
包作为插件,打包很简单方便,缺点就是文件大。使用apk
的话就没必要是将dex
插入dexElements
里面去,直接将之前的dexElements
替换就可以了。下面看一下apk
插件的具体实现
apk插件的实现
// apk作为插件加载
private void apkPlugin() {
//插件包文件
File file = new File("/sdcard/FixDexTest.apk");
if (!file.exists()) {
Log.i("MApplication", "插件包不在");
return;
}
try {
//获取到 BaseDexClassLoader 的 pathList字段
// private final DexPathList pathList;
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
//破坏封装,设置为可以调用
pathListField.setAccessible(true);
//拿到当前ClassLoader的pathList对象
Object pathListObj = pathListField.get(getClassLoader());
//获取当前ClassLoader的pathList对象的字节码文件(DexPathList )
Class<?> dexPathListClass = pathListObj.getClass();
//拿到DexPathList 的 dexElements字段
// private final Element[] dexElements;
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
//破坏封装,设置为可以调用
dexElementsField.setAccessible(true);
//使用插件创建 ClassLoader
DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
//拿到插件的DexClassLoader 的 pathList对象
Object newPathListObj = pathListField.get(pathClassLoader);
//拿到插件的pathList对象的 dexElements变量
Object newDexElementsObj = dexElementsField.get(newPathListObj);
//将插件的 dexElements对象设置给 当前ClassLoader的pathList对象
dexElementsField.set(pathListObj, newDexElementsObj);
} catch (Exception e) {
e.printStackTrace();
}
}
常用第三方热修复组件
常用优秀第三方热修复组件可以分为俩大类
- Dex插桩(类替换)
Tinker:微信
Qzone:QQ空间
Robust:美团 - 底层替换方案,类似反射
阿里的AndFix
这俩种不同方案各有优缺点,大家可以更具实际情况选择使用的方式。使用方法可以参考各自的官网和开发社区。
总结
经过对 PathClassLoader
、 DexClassLoader
、 BaseDexClassLoader
、 DexPathList
的分析,我们知道,安卓的类加载器在加载一个类时会先从自身DexPathList
对象中的Element
数组中获取( Element[]``dexElements
)到对应的类,之后再加载。采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex
文件。在for循环中,首先遍历出来的是dex
文件,然后再是从dex
文件中获取class
,所以,我们只要让修复好的class
打包成一个dex
文件,放于 Element
数组的第一个元素,这样就能保证获取到的class
是最新修复好的class
了(当然,有bug
的class
也是存在的,不过是放在了 Element
数组的最后一个元素中,这样就避免拿到有bug
的class
。