这个方案是基于Android的类加载机制
PathClassLoader负责加载Android的系统类和你自己定义的应用程序类;
DexClassLoader负责加载任意目录下的dex和zip、jar、apk内的dex文件;
PathClassLoader0和DexClassLoader都继承自BaseDexClassLoader,这两者类似,只是增加了调用父类构造函数的构造函数。所以功能不同只是因为定义,是设计人员要求这做,其实PathClassLoader能做的,DexClassLoader也能做
类加载流程
首先我们要了解类加载的流程,加载类的方法就是loadClass()
- 调用loadClass(),查看此类是否已经被加载过;是,直接返回结果;不是,跳到步骤2
- 如果父类不为null,调用父类加载器的
loadClass()
;如果父类为null,调用当前类加载器的findClass()
加载,这是真正加载类的方法。 - 最后返回结果
这里的父类指的是类加载其中的属性ClassLoader parent;
而不是继承关系。
BaseDexClassLoader继承了ClassLoader,重写了findClass()
。我们看看这个方法干了什么。
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
重要的是这一行Class c = pathList.findClass(name, suppressedExceptions);
,很显然,类加载的操作是在这里完成的。
pathList是BaseDexClassLoader的一个属性,pathList里面还有一个Element数组,你可以把每一个Elment看成是一个.dex文件。
//DexPathList.class
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;
}
pathList遍历了Element数组,依次调用Element.findClass()
,这个源码就不看了,反正如果这个Element元素是我们要加载的类就会返回正确结果,否则继续下一个。
那我们现在就知道了,Android系统和应用程序类加载时,会去加载pathList内的一个个Element,如果找到对应的.dex就会返回。
那么我们就可以利用这一点,首先DexClassLoader跟PathClassLoader都继承自BaseDexClassLoader,所以我们可以利用DexClassLoader去加载我们需要替换的新类,然后通过反射获得DexClassLoader和PathClassLoader的pathList,再获得Element集合,最后合并两个集合,这里要注意,DexClassLoader的Element集合需要放在前面。
这就是代码替换的原理。