Android 项目的.java文件会打包成.dex文件,那么Android程序在运行的时候,是如何从dex中找到我们的java类,然后加载到程序中的呢?下面我们来了解下。
一、Android中的类加载器
Android 中类加载器有三种
- BootClassLoader
用于加载Android Framework层class文件,比如Activity,View类。 - PathClassLoader
用于加载Android Framework之外的第三方插件的类或者是由开发者自己写的类,比我我们写的Main。也可以加载指定的dex,以及jar、zip、apk中的classes.dex - DexClassLoader
用于加载指定的dex,以及jar、zip、apk中的classes.dex
看看PathClassLoader和DexClassLoader的区别
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
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 中,optimizedDirectory是优化处理dex文件后的文件存放路径,而PathClassLoader 没有传的话,优化处理dex文件后的文件是默认存放到/data/dalvik-cache/目录,所以这两者基本上也没什么不同。
然后看看类的继承关系
可以看到,PathClassLoader和DexClassLoader都是BaseDexClassLoader的子类。
二、双亲委托机制
在MainActivity中写写如下代码
ClassLoader mainActivityclassLoader = MainActivity.class.getClassLoader();
Log.d(TAG, "mainActivityclassLoader: "+mainActivityclassLoader);
Log.d(TAG, "mainActivityclassLoader.parent: "+mainActivityclassLoader.getParent());
ClassLoader activityclassLoader = Activity.class.getClassLoader();
Log.d(TAG, "activityclassLoader: "+activityclassLoader);
Log.d(TAG, "activityclassLoader.parent: "+activityclassLoader.getParent());
看输出
com.example.hook D/MainActivity: mainActivityclassLoader :dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.hook-PrrYjr-tl8XlzcVWdVe8Rw==/base.apk"],nativeLibraryDirectories=[/data/app/com.example.hook-PrrYjr-tl8XlzcVWdVe8Rw==/lib/x86, /system/lib]]]
com.example.hook D/MainActivity: mainActivityclassLoader.parent :java.lang.BootClassLoader@57fe44d
com.example.hook D/MainActivity: activityclassLoader :java.lang.BootClassLoader@57fe44d
com.example.hook D/MainActivity: activityclassLoader.parent :null
这里可以看出PathClassLoader的parent(父亲)是BootClassLoader。这里的父亲并不是java中的父类,千万不要搞混了。这里的parent只是classLoader中的一个属性。
那么什么是双亲委托机制呢?
当某个类加载器需要加载某个class类时,它首先把这个任务委托给他的父亲加载器,递归这个操作,如果父亲的类加载器没有加载,自己才会去加载这个类。
这里需要记录下ClassLoader的parent关系
- DexClassLoader的parent是PathClassLoader
- PathClassLoader的parent是BootClassLoader
我们再看看loadClass的源码
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
// 检查class是否有被加载
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果parent不为null,则调用parent的loadClass进行加载
c = parent.loadClass(name, false);
} else {
//parent为null,则调用BootClassLoader进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果都找不到就自己查找
long t1 = System.nanoTime();
c = findClass(name);
}
}
return c;
}
这里我们也看到了双亲委托机制对应的代码。先调用parent.loadClass()找对应的类,找不到在调用自己的findClass()。
然后我们跟源码看看,findClass是直接调用到BaseDexClassLoader的findClass,然后是调用调DexPathList.java的findClass方法中
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;
}
这里可以看出是通过遍历dexElements,我们再看看dexElements是什么?
public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexFiles == null) {
throw new NullPointerException("dexFiles == null");
}
if (Arrays.stream(dexFiles).anyMatch(v -> v == null)) {
throw new NullPointerException("dexFiles contains a null Buffer!");
}
this.definingContext = definingContext;
// TODO It might be useful to let in-memory dex-paths have native libraries.
this.nativeLibraryDirectories = Collections.emptyList();
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);//看这里
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
在在构造方法中,我们可以看出dexElements就是加载的dex文件的一个数组。到这里思路就应该可以理清了,我们项目里面的.class文件会打包的dex文件里面。dex文件会加载到DexPathList的dexElements数组里面,然后每次查找要加载的类就直接来dexElements找。
热修复原理就是把修复好bug的.java类打包成dex文件,然后通过反射添加到dexElements数组的最前面,程序后续再按类名加载类的时候,就会先加载到我们插入到dexElements的已修复bug的类。从而完成热修复。