Android热修复之路——ClassLoader详解

3,520 阅读9分钟

1、ClassLoader简介

在应用程序打包成APK时,程序中所创建的类、导入和引用的类都会被编译打包成一个或多个的dex文件中,打包成的dex文件在使用中如何加载类?答案就在本篇主题ClassLoader中,ClassLoader从字面就可以知道它主要用于类的加载,当代码中需要调用某个类时,ClassLoader就会遍历所有的dex文件中的类并保存在集合中,然后从集合中加载指定的Class文件,最终转换成JVM中使用的类;

2、ClassLoader工作机制

  • Android中ClassLoader分类
  1. PathClassLoader:负责加载系统和apk中的类,context.getClassLoader获取到的就是PathClassLoader实例;
  2. DexClassLoader:负责加载外部类(jar或apk),也用于热修复方案的执行;如:修复的dex文件;
  3. BaseDexClassLoader:PathClassLoader 和 DexClassLoader的父类,主要的执行逻辑和文件的处理都在其中(后面会分析它的源码);
  4. BootClassLoader:继承与ClassLoader类,一般来说向上传递时最顶层就是BootClassLoader,它在Zygote进程启动开始时,在ZygoteInit.main()方法中执行资源预加载,此时会单例创建BootClassLoader对象,它在loadClass中直接调用findClass(),而findClass()中调用Class.classForName(name, false, null)查找类;
  • 获取ClassLoader的继承关系
var loader = classLoader
while (loader != null) {
      System.out.println(loader)
      loader = loader.parent
   }
  1. 获取输出的结果:PathClassLoader中parent为BootClassLoader,在PathClassLoader内部保存这base.apk和library的文件
2019-08-29 13:13:10.444 29022-29022/com.alex.kotlin.optimization I/System.out: dalvik.system.PathClassLoader
[DexPathList[
[zip file "/data/app/com.alex.kotlin.optimization-lLeC3751i-Krivn3eNgrYA==/base.apk"],
nativeLibraryDirectories=[/data/app/com.alex.kotlin.optimization-lLeC3751i-Krivn3eNgrYA==/lib/arm64, /system/lib64, /system/vendor/lib64]]]
2019-08-29 13:13:10.444 29022-29022/com.alex.kotlin.optimization I/System.out: java.lang.BootClassLoader@14d954f
  1. 源码继承关系
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 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!");
    }
}
  • ClassLoader 传递性 虚拟机的加载策略:在触发虚拟机对类的加载时,虚拟机默认使用调用对象中的ClassLoader加载,而调用对象又被调用它的对象中的ClassLoader加载,按此传递最终执行到最上层的ClassLoader

  • 双亲委派机制

  1. 在每个ClassLoader中都持有其父类的引用,在加载类文件时,首先会判断当前ClassLoader中是否已经加载过此类,如果加载过从缓存中获取,否则调用其父类的加载器去查找,父类也会先检查自己的缓存然后再调用父类查找,直到调用到BootstrapClassLoader(它内部持有的父类为空),当BootstrapClassLoader没有找到会向下逐级触发子类ClassLoader的查找,直到发起者
  2. 双亲委托机制的好处:(1)先执行父类的查找,避免了同一个类的多次加载;(2)这种先从父类查找的机制,使程序无法修改和替代Java基础包中的类,提高程序的安全性

3、ClassLoader源码分析

  • loadClass():ClassLoader中加载类时调用loadClass(),代码中将双亲机制体现的淋漓尽致
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        Class<?> c = findLoadedClass(name);  //查找是否加载过此类
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);  //调用父类ClassLoader加载
                } else {
                    c = findBootstrapClassOrNull(name); //父类为null,表示为BootstrapClassLoader
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {  //父类查找为null,调用自己的查找
                c = findClass(name);
            }
        }
        return c;
}

工作流程:

  1. 在loadClass中首先根据类名判断虚拟机是否加载过此类,如果加载过则从中获取
  2. 对于首次加载的类,先调用ClassLoaser的父类进行加载,如果为发现继续向上查询直到顶层的ClassLoader即BootClassLoader
  3. 如果都为发现则从顶层向下逐层的子ClassLoader调用findClass()查找并加载类
  • findClass():根据文件名称查找类文件,此方法主要给字类重写,实现具体的查找功能,稍后在BaseDexClassLoader中即可看出其中的用法;
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

//BootClassLoader 中实现的方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    return Class.classForName(name, false, null);  //调用native方法
}

4、BaseDexClassLoader源码分析

由上面的分析知道PathClassLoader和DexClassLoader都是BaseDexClassLoader的自类,PathClassLoader用于加载系统和app中的文件,在Zygote进程中启动系统服务时创建,DexClassLoader负责加载指定目录中的文件,从二者的代码中也可以看出都是直接使用BaseDexClassLoader中的方法,所以程序中类的加载基本都是BaseDexClassLoader在工作,以下一起看看BaseDexClassLoader的工作原理

  • 构造函数
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
          String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);//创建DexPathList实例
    }

从代码中看出BaseDexClassLoader的构造函数中创建DexPathList的实例,将然如的参数封装在DexPathList当中,其实这里不只是创建实例而是执行了整个过程,先看看传入这几个参数的意义:

  1. this:ClassLoader对象,在DexPathList保存并使用
  2. dexPath:dex文件的路径,主要加载dex、apk等
  3. librarySearchPath:本地文件路径,主要加载程序引入的so库
  4. optimizedDirectory:加载dex文件优化后的文件存储路径,dex处理过的文件保存在此文件夹下
  • findClass():调用pathList中的方法查找Class
@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
       Class c = pathList.findClass(name, suppressedExceptions); // 委托pathList查找类文件
       return c;
    }

在findClass()方法中调用pathList的方法,所以整个逻辑的执行好像都在pathList中,这里先来看看DexPathList中的基本属性,具体看代码中注释:

private static final String DEX_SUFFIX = ".dex”; //dex文件后缀
private static final String zipSeparator = "!/“; //zip文件后缀

private final ClassLoader definingContext; //执行加载的ClassLoader,在创建时赋值

private Element[] dexElements; //查找之后保存的Element数组
private final Element[] nativeLibraryPathElements; // 本地的Elements列表
private final List<File> nativeLibraryDirectories;
private final List<File> systemNativeLibraryDirectories;
private IOException[] dexElementsSuppressedExceptions;

在DexPathList的构造函数中直接调用了makeDexElements(),整个加载解析的过程都在这个方法中,会遍历文件集合中的文件,找出jar、apk、dex文件并保存在Element中,最后返回Element数组;

  • makeDexElements()
private static Element[] makeElements(List<File> files, File optimizedDirectory,
285                                          List<IOException> suppressedExceptions,
286                                          boolean ignoreDexFiles,
287                                          ClassLoader loader) {
288        Element[] elements = new Element[files.size()]; //创建Element集合
289        int elementsPos = 0; 
294        for (File file : files) { //循环便利所有的File
295            File zip = null;  //创建Zip、dir、DexFile实例
296            File dir = new File("");
297            DexFile dex = null;

298            String path = file.getPath(); //获取文件的路径和名称
299            String name = file.getName();
300
301            if (path.contains(zipSeparator)) { //处理zip文件后缀
302                String split[] = path.split(zipSeparator, 2);
303                zip = new File(split[0]);
304                dir = new File(split[1]);
305            } else if (file.isDirectory()) {
308                elements[elementsPos++] = new Element(file, true, null, null); //保存目录文件到Elements集合中
309            } else if (file.isFile()) {
310                if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) { //处理.dex文件后缀
312                    try {
313                        dex = loadDexFile(file, optimizedDirectory, loader, elements); //创建DexFile实例
314                    } catch (IOException suppressed) {
316                        suppressedExceptions.add(suppressed);
317                    }
318                } else {
319                    zip = file; 
321                    if (!ignoreDexFiles) {
322                        try {
323                            dex = loadDexFile(file, optimizedDirectory, loader, elements);
324                        } catch (IOException suppressed) {
332                            suppressedExceptions.add(suppressed);
333                        }
334                    }
335                }
336            } 
339
340            if ((zip != null) || (dex != null)) {  //保存Element
341                elements[elementsPos++] = new Element(dir, false, zip, dex);
342            }
343        }
344        if (elementsPos != elements.length) {
345            elements = Arrays.copyOf(elements, elementsPos);
346        }
347        return elements;
348    }

具体执行细节见代码中注释,这里简单总结一下:makeElements()方法中遍历传入的文件集合,查找集合中所有的apk、jar、dex文件及文件夹下所有的dex文件,对于文件目录直接创建Element封装文件,对于dex、apk、jar文件则创建DexFile封装文件、ClassLoader、本地目录、Elements信息,然后将DexFile和文件封装成Element保存,因为最后的文件加载是获取Elements的集合中保存Element实例,然后调用实例中的DexFile进行加载;

  • DexPathList中Class加载过程
public Class findClass(String name, List<Throwable> suppressed) {
414        for (Element element : dexElements) { //遍历DexFileelements数组
415            DexFile dex = element.dexFile; //获取Element中的DexFile
417            if (dex != null) { //调用DexFile方法加载Class类,对于目录来说此时dex = null
418                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
419                if (clazz != null) {
420                    return clazz;
421                }
422            }
423        }
424        if (dexElementsSuppressedExceptions != null) {
425            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
426        }
427        return null;
428    }

由前面学习知道Class的加载是在DexPathList.findClass()中执行的,在findClass()中主要助兴:

  1. 根据前面获取到的所有Elements的集合,遍历取出集合中的Element,从取出的Element中获的DexFile实例,然后调用DexFile.loadClassBinaryName()加载类
  2. 在DexFile.loadClassBinaryName()中调用native方法defineClassNative(name, loader, cookie, dexFile);进行类的加载,程序由此进入Native层,这里不做讨论
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,DexFile dexFile)

5、利用ClassLoader加载补丁文件

由ClassLoader加载机制知道,再查找类时不管子类或是父类只返回最先查到的一个,即DexPathList中保存Elemets集合中靠前的一个,而且系统提供了DexClassLoader让我们自己加载dex文件,那么从这原理我们可以发现似乎我们有操作和替换系统类的机会,也的确如此,热修复的原理就是替换ClassLoader中解析出的Elements中顺序,让修复后的类被优先加载,从而抛弃有Bug的类,如QQ的超级补丁

将这里根据上面的知识编写dex文件加载工具类,在编写代码之前先分析一下代码执行的逻辑:

  1. 创建ClassLoader实例,并使用其加载指定路径的dex文件获取Elements数组
  2. 获取系统自身的ClassLoader中的Elements数组
  3. 组合前面获取的数组
  4. 反射设置pathList的新数组
  • 加载路径下的dex文件
//创建ClassLoader传入dex文件路径即可完成加载
DexClassLoader dexClassLoader = new DexClassLoader(path, optDir, path, getPathClassLoader());

  • 反射获取上面加载获得的Elements数组
Object elementsNew = getElements(getPathList(dexClassLoader));

//从dexClassLoader实例中创建的pathList
private static Object getPathList(BaseDexClassLoader classLoader) {
		Object o = null;
		try {
			Field pathList = classLoader.getClass().getField("pathList"); //反射获取pathList
			o = pathList.get(classLoader);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return o;
	}
// 从PathList中获取加载的Elements数组
private static Object getElements(Object pathList) {
		Object o = null;
		try {
			Field pathListField = pathList.getClass().getField("dexElements");//反射
			o = pathListField.get(pathList);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return o;
	}

  • 使用同样的方式获取系统PathClassLoader中的Elements
Object elements = getElements(getPathList(getPathClassLoader()));

private static PathClassLoader getPathClassLoader() {
		return (PathClassLoader) DexUtils.class.getClassLoader();
	}
  • 组合Elements数组,Newa中采取的方式是将加载的Path中的Elements数组放在系统加载的数组前面,即可在查找时获得新的数组中的类,实现热修复
private static Object combineArray(Object firstArray, Object secondArray) {
        Class<?> localClass = firstArray.getClass().getComponentType();
        int firstArrayLength = Array.getLength(firstArray);
        int allLength = firstArrayLength + Array.getLength(secondArray);
        Object result = Array.newInstance(localClass, allLength);
        for (int k = 0; k < allLength; ++k) {
            if (k < firstArrayLength) {
                Array.set(result, k, Array.get(firstArray, k));
            } else {
                Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
            }
        }
        return result;
    }
  • 反射设置组合后的Elements数组
private static void setElements(Object pathList, Object o) {
		try {
			Field pathListField = pathList.getClass().getField("dexElements");
			pathListField.set(pathList, o);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

到此关于ClassLoader的介绍就结束了,认识ClassLoader加载机制对热修复的学习有很大的帮助,本篇也作为热修复学习的第一篇,之后会继续更新热修复的学习;