前言
最近在看Tinker的原理,发现核心是通过ClassLoader做的,由于之前也从未接触过ClassLoader趁着上周末看了安卓ClassLoader相关源码,这里分享一发安卓的ClassLoader和热更新的实现原理。
ClassLoader
首先我们要知道,程序在运行时要把对应的类加载到内存,在安卓上来说就是把Dex文件中的类加载到内存,这个加载流程是通过ClassLoader实现的。因此如果我们想要动态加载自己的类,就得从ClassLoader上做文章,那么接下我们先看看安卓的ClassLoader。
public abstract class ClassLoader {
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
可以看到在构造方法的时候我们需要传入一个父ClassLoader,默认没传情况下是以PathClassLoader作为父加载器的。
然后加载类是调用的loadClass()方法,他是先判断是本ClassLoader是否已经加载过,如果没加载过就让父加载器加载,然后父加载器看自己是否加载过,如果没加载过让祖父加载器加载,然后祖父加载器没找到,父加载器在找,父加载器没找到,子加载器再找,这也就是我们常说的双亲委派模式。
采取双亲委派模式主要有两点好处:
- 避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是先从缓存中直接读取。
- 更加安全,如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类,除非我们修改类加载器搜索类的默认算法。还有一点,只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类。
那么下面的重点是看Android的类加载器如何重写findClass()方法来加载类的。
Android ClassLoader继承关系
从上图可以看到与ClassLoader相关的一共有7个,大致可以分为如下3类
-
BootClassLoader是ClassLoader内部类,是Android中所有ClassLoader的parent,可见性为包内可见我们无法使用。 -
BaseDexClassLoader是PathClassLoader、DexClassLoader、InMemoryDexClassLoader的父类,类加载的主要逻辑都是在BaseDexClassLoader完成的。 -
SecureClassLoader继承了抽象类ClassLoader,拓展了ClassLoader类加入了权限方面的功能,加强了安全性,其子类URLClassLoader是用URL路径从jar文件中加载类和资源,我们用不上。
我们重点关注与我们热更新相关的PathClassLoader和DexClassLoader。
-
PathClassLoader只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。 -
DexClassLoader可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。
这里我们可以验证下PathClassLoader是安卓默认使用的类加载器。在Activity的onCreate()方法中打印它的类加载器。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i("zhuliyuan", this.getClass().getClassLoader().toString());
}
源码查看(基于API28)
/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*/
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);
}
}
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*/
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
从注释上可以看出PathClassLoader是用于系统类和应用程序类的加载,DexClassLoader可以用来加载任意目录的dex。具体实现还得看BaseDexClassLoader的构造方法。
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
...
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
...
Class c = pathList.findClass(name, suppressedExceptions);
...
return c;
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
}
可以看出在构造方法中创建了一个DexPathList对象赋值给了pathList字段,然后findxxx()方法都是从DexPathList中查找。
BaseDexClassLoader的构造函数包含四个参数:
- dexPath:包含类和资源的jar / apk文件列表,由
File.pathSeparator分隔,在Android上默认为:。 - optimizedDirectory:由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径。这也是对apk中dex根据平台进行ODEX优化的过程。自API26开始无效。
- librarySearchPath:指目标类中所使用的C/C++库存放的路径,可以为null。
- parent:父
ClassLoader引用。
接下来我们查看DexPathList的构造方法和findxxx()方法。
final class DexPathList {
private Element[] dexElements;
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
...
// 加载dexPath路径下的dex和resource
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
...
}
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;
for (File file : files) {
if (file.isDirectory()) {
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) {
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
return elements;
}
}
构造方法中调用makeDexElements()方法获取到了Element[]数组赋值给了dexElements变量。
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
而findClass()方法则是变量在构造方法初始化好的Element[]数组中从前往后遍历找到我们需要的类。
这里我们总结下类加载的过程
-
PathClassLoader和DexClassLoader调用了父类BaseDexClassLoader的构造方法。 -
BaseDexClassLoader在构造方法中创建了DexPathList对象并赋值给pathList字段,加载类的findxxx()方法都是调用DexPathList类的findxxx()方法来实现内的加载。 -
DexPathList在构造方法中调用makeDexElements()方法创建了Element[]数组赋值给dexElements字段,findClass()方法就是从前往后遍历Element[]数组找到我们要的class.
热更新实现原理
上面已经分析了类的加载流程,那么要想实现热更新我们需要拿到PathClassLoader中的DexPathList对象然后在拿到他当中的Element[]数组将我们已经修复的Element插入到数组的前面,这样在类加载时候就会先拿到我们已经修复的class了达到动态替换的效果。
加载我们自己的dex文件需要用到DexClassLoader,然后通过反射取出DexClassLoader中的DexPathList对象中的Element[]数组插入到PathClassLoader中Element[]数组前面即可。
热更新实际例子的话我会在下篇博客给出敬请期待嘿嘿嘿。