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/目录,所以这两者基本上也没什么不同。

然后看看类的继承关系

Android 页面打开慢 显示一个加载进度条 android 加载更多_sed


可以看到,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的类。从而完成热修复。