前言
关于Android热修复技术的博文网上可以搜到一大堆,原理比较简单,相关案例也讲的比较详细。虽说原理简单但是涉及到很多理论基础,下面笔者会基于自己的理解对其中涉及到的知识点做一下详细的总结。
提示:以下是本篇文章正文内容,下面案例可供参考
一、类的加载过程
深入jvm一书中对此做了明确描述:
1、加载:
将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口
2、链接
将Java类的二进制代码合并到JVM的运行状态之中。
- 验证
确保加载的类信息符合JVM规范,没有安全方面的问题。
- 准备
正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注意此时的设置初始值为默认值,具体赋值在初始化阶段完成
- 解析
虚拟机常量池内的符号引用替换为直接引用(地址引用)的过程。
3、初始化
初始化阶段才真正到了类中定义的java代码的阶段,在这个阶段会对类中的变量和一些代码块进行初始化,比如以类变量进行初始化,在准备阶段对类变量进行的默认初始化,到这个阶段就对对变量进行显式的赋值,其中静态代码块就是在这个阶段来执行的。
二、Android中的类加载机制
掌握热修复技术,还需要了解一下Android的类加载机制————双亲委托
何为双亲委托?直接上代码:
loadclass方法就是类加载的入口,findloaderclass表示从某个classloader实例中寻找已经被加载的类(该类有可能没有被加载没有,则返回null)。
整个大概的逻辑:先从当前的classloader寻找这个类,如果找到说明这个类已经被加载过了,那就直接用;如果没找到,则从他的父classloader中继续找,一直找到根classloader。最后如果还是没有找到,则用当前的classloader加载此类。
我们都知道(真的知道吗?)Android中的类加载器有三种:pathclassloader、dexclassloader和bootclassloader。前两个是app级别的,bootclassloader是系统的。
pathclassloader、dexclassloader继承了basedexclassloader,basedexclassloader继承了classloader,而bootclassloader直接继承classloader
假设我们app中要加载一个我们自定义的一个类时(比如:new XXX()),那么依照上面的寻找逻辑,应该从哪个classloader开始找呢?他们的子父关系又是如何的呢?
我们用demo来说明:
通过断点我们可以看出在加载A的时候,寻找顺序是pathclassloader——>bootclassloader。
我们知道A类是我们自定义的,最终存在apk的dex中,由pathclassloader加载。
那么哪些类是由bootclassloader加载的呢?
修改代码,断点:
没错,就是我们常见的String类,由系统的bootclassloader加载。
其实以上我们只是分析了Android中类加载流程的查找链,下面我们再具体分析一下pathclassloader是如何加载一个从未加载过的类的。
继续分析findClass方法:
直接抛出异常?其实是被Basedexclassloader重写了,转到Basedexclassloader:
再看看pathclassloader的源码:
这样就清楚了,实际上是pathclassloader.findclass......而findclass方法中又用到了一个叫pathlist的对象去findclass。pathlist在构造函数中被赋值,类型为DexPathList。
DexPathList.java:
这里我们主要关注 dexElements
DexPathList的构造函数部分参数说明:
definingContext:当前的classloader对象(demo场景中是pathclassloader)
dexPath:apk的安装路径 e.g. /data/app/com.example.xxx-nFU7pNlE0rvl4EttvuvQnA==/base.apk
librarySearchPath:共享库的路径解压路径
optimizedDirectory:dex文件优化后的路径(最新源码这个字段已经废弃,默认为null,最后dexPath文件的优化路径跟随dexppath)
接下来看makeDexElments方法:
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* Open all files and load the (direct or contained) dex files up front.
*/
for (File file : files) {
if (file.isDirectory()) {
// We support directories for looking up resources. Looking up resources in
// directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if
* the zip file turns out to be resource-only (that is, no classes.dex file
* in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
makeDexElments实现了将dexfile存储到dexelements这个数组中。
数据结构如下图:
然后看一下findclass的调用链:
DexPathList#findclass
DexPathList$Element#findclass
最终我们发现系统还是从dexfile中去加载class文件。
以上就是双亲委托机制的基本雏形,因为是双亲委托机制,保证了类加载的唯一性(相同全类限定名并且被同一个classloader加载),这一点dalvik和标准的jvm是不一样的。
三、实现热修复
好,有了以上的理论基础我们要去实现一个简单的热修复就不难了。
不过在此之前还是先提一个东西,就是dexclassloader,其实dexclassloader和pathclasslaoder类似,API26以前唯一的区别就是:
1、DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk
2、PathClassLoader只能加载系统中已经安装过的apk
但是从26开始就没有区别了,从源代码也能看出来。
言归正传,如何实现热修复?
很简单,把准备好的需要更新dex通过dexclassloader构造得到dexelements,然后插入app的classloader(pathclassloader)中dexelements数组前端即可,如下图:
这样就可以先加载需要更新的类,达到覆盖旧的类的目的。
相关实现代码网上很多,这里就不贴了,大家自行学习。
总结
其实热更新技术背后还衍生出了很多问题需要探讨,比如app的classloader怎么来的?如何插件化资源?关于以上这些问题我们后面再继续深入剖析。