bug通常是一个或多个class出现了问题,在一个理想的状态下,咱们只需将修复好的这些个class更新到用户手机上的app中就能够修复这些bug了。要怎么才能动态更新这些class呢?其实,不论是哪一种热修复方案,确定是以下几个步骤:java
这里的某种方式,对本篇而言,就是使用Android的类加载器,经过类加载器加载这些修复好的class,覆盖对应有问题的class,理论上就能修复bug了。android
在加载一个字节码文件时,会询问当前的classLoader是否已经加载过此字节码文件。若是加载过,则直接返回,再也不重复加载。若是没有加载过,则会询问它的Parent是否已经加载过此字节码文件,一样的,若是已经加载过,就直接返回parent加载过的字节码文件,而若是整个继承线路上的classLoader都没有加载过,才由child类加载器(即,当前的子classLoader)执行类的加载工做。数组
若是一个类被classLoader继承线路上的任意一个加载过,那么在之后整个系统的生命周期中,这个类都不会再被加载,大大提升了类的加载效率。缓存
一些Framework层级的类一旦被顶层classLoader加载过,会缓存到内存中,之后在任何地方用到,都不会去从新加载。安全
共同继承线程上的classLoader加载的类,确定不是同一个类,这样能够避免某些开发者本身去写一些代码冒充核心类库,来访问核心类库中可见的成员变量。如java.lang.String在应用程序启动前就已经被系统加载好了,若是在一个应用中可以简单的用自定义的String类把系统中的String类替换掉的话,会有严重的安全问题。bash
验证多个类是同一个类的成立条件:服务器
经过loadClass()这个方法来验证双亲委派模型网络
找到ClassLoader这个类中的loadClass()方法,它调用的是另外一个2个参数的重载loadClass()方法。app
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
复制代码
找到最终这个真正的loadClass()方法,下面即是该方法的源码:jvm
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;
}
复制代码
能够看到,如前面所说,加载一个类时,会有以下3步:
Android跟java有很大的渊源,基于jvm的java应用是经过ClassLoader来加载应用中的class的,Android对jvm优化过,使用的是dalvik虚拟机,且class文件会被打包进一个dex文件中,底层虚拟机有所不一样,那么它们的类加载器固然也是会有所区别。
Android中最主要的类加载器有以下4个:
一个app必定会用到BootClassLoader、PathClassLoader这2个类加载器,可经过以下代码进行验证:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = getClassLoader();
if (classLoader != null) {
Log.e(TAG, "classLoader = " + classLoader);
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
Log.e(TAG, "classLoader = " + classLoader);
}
}
}
复制代码
上面代码中能够经过上下文拿到当前类的类加载器(PathClassLoader),而后经过getParent()获得父类加载器(BootClassLoader),这是因为Android中的类加载器和java类加载器同样使用的是双亲委派模型。
通常的源码在Android Studio中能够查到,但 PathClassLoader 和 DexClassLoader 的源码是属于系统级源码,因此没法在Android Studio中直接查看。能够到androidxref.com这个网站上直接查看,下面会列出以后要分析的几个类的源码地址。
如下是Android 5.0中的部分源码:
先来介绍一下这两种Classloader在使用场景上的区别
下面来看一下PathClassLoader与DexClassLoader的源码的差异,都很是简单
// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
复制代码
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
复制代码
经过比对,能够得出2个结论:
经过观察PathClassLoader与DexClassLoader的源码咱们就能够肯定,真正有意义的处理逻辑确定在BaseDexClassLoader中,因此下面着重分析BaseDexClassLoader源码。
先来看看BaseDexClassLoader的构造函数都作了什么:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
...
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
...
}
复制代码
tip:从一个完整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 + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
复制代码
能够看到,BaseDexClassLoader的findClass()方法其实是经过DexPathList对象(pathList)的findClass()方法来获取class的,而这个DexPathList对象刚好在以前的BaseDexClassLoader构造函数中就已经被建立好了。因此,下面就来看看DexPathList类中都作了什么。
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
...
}
复制代码
这个构造函数中,保存了当前的类加载器definingContext,并调用了makeDexElements()获得Element集合。
经过对splitDexPath(dexPath)的源码追溯,发现该方法的做用其实就是将dexPath目录下的全部程序文件转变成一个File集合。并且还发现,dexPath是一个用冒号(":")做为分隔符把多个程序文件目录拼接起来的字符串(如:/data/dexdir1:/data/dexdir2:...)。
那接下来无疑是分析makeDexElements()方法了,由于这部分代码比较长,我就贴出关键代码,并以注释的方式进行分析:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
// 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的构造函数是将一个个的程序文件(多是dex、apk、jar、zip)封装成一个个Element对象,最后添加到Element集合中。
其实,Android的类加载器(不论是PathClassLoader,仍是DexClassLoader),它们最后只认dex文件,而loadDexFile()是加载dex文件的核心方法,能够从jar、apk、zip中提取出dex,但这里先不分析了,由于第1个目标已经完成,等到后面再来分析吧。
再来看DexPathList的findClass()方法:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
// 遍历出一个dex文件
DexFile dex = element.dexFile;
if (dex != null) {
// 在dex文件中查找类名与name相同的类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
复制代码
结合DexPathList的构造函数,其实DexPathList的findClass()方法很简单,就只是对Element数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。
为何是调用DexFile的loadClassBinaryName()方法来加载class?这是由于一个Element对象对应一个dex文件,而一个dex文件则包含多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件。这能够从Element这个类的源码和dex文件的内部结构看出。
通过对PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,咱们知道,安卓的类加载器在加载一个类时会先从自身DexPathList对象中的Element数组中获取(Element[] dexElements)到对应的类,以后再加载。采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex文件。在for循环中,首先遍历出来的是dex文件,而后再是从dex文件中获取class,因此,咱们只要让修复好的class打包成一个dex文件,放于Element数组的第一个元素,这样就能保证获取到的class是最新修复好的class了(固然,有bug的class也是存在的,不过是放在了Element数组的最后一个元素中,因此没有机会被拿到而已。