1. 类加载机制
1.1 类加载UML图
1.2 loadClass流程
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) {
if (parent != null) {
//使用父ClassLoader加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
首先先判断是否已经加载过这个类
如果没有加载过就让父加载器来加载
-
如果父加载器没有加载到才使用当前的ClasLoader来加载----双亲委派机制
- 避免多次加载
- 避免系统class被自定义的ClasLoader篡改
-
findClass方法由ClassLoader的子类实现,主要由
BaseDexClassLoader
类实现@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; }
在
BaseDexClassLoader
类中包含一个DexPathList
对象在这个
pathList
中查找对应的加载类
DexPathList
中通过一个Element
数组保存加载过的类,然后遍历这个数组当找到从找到一个和当前类名匹配的Class
对象时就停止遍历,返回这个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; }
2. 实现原理
主要流程
- 将新的dex文件转换成Element数组
- 获取新生成的Element数组中的所有对象
- 获取旧的Element数组中的对象
- 将新的对象插入到旧的对象之前,在遍历时就能获取到最新的class文件
- 把组合的Element数组设置到DexPathList对象中
2.1 从类加载器中获取DexPathList
对象
Class<?> cl = null;
Field pathListFiled = null;
for (cl = classLoader.getClass(); cl != null; cl = cl.getSuperclass()) {
try {
//从BaseDexClassLoader中找pathList属性
pathListFiled = cl.getDeclaredField("pathList");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
if (pathListFiled != null) {
pathListFiled.setAccessible(true);
break;
}
}
因为只有BaseDexClassCloader
中才持有这个对象,因此需要对当前ClassLoader的父类进行遍历,通过反射拿到DexPathList
属性对象
2.2 获取makePathElements方法
通过反射从DexPathList
对象中获取makePathElements
这个方法,通过这个方法把传入的Dex文件转换成Element数组。
Object pathList = pathListFiled.get(classLoader);
Class<?> pathListClass = pathList.getClass();
Method makePathListMethod = pathListClass.getDeclaredMethod("makePathElements",
List.class, File.class, List.class);
makePathListMethod.setAccessible(true);
//要插入的新的文件集合
List<File> dexFile = new ArrayList<>();
File f = new File(file);
dexFile.add(f);
List<IOException> suppressedException = new ArrayList<>();
//获得新的dexElements数组
Object[] elements = (Object[]) makePathListMethod.invoke(pathList, dexFile,new File(optPath),
suppressedException);
Log.d("sxhLog", "fix: " + elements.length);
2.3 获取PathList中原有的Element数组
直接通过反射拿到PathList
对象中的dexElement
对象。
Field oldElementsFiled = pathListClass.getDeclaredField("dexElements");
oldElementsFiled.setAccessible(true);
Object[] oldElements = (Object[]) oldElementsFiled.get(pathList);
Log.d("sxhLog", "fix: " + oldElements.length);
2.4 将新的数组插入旧的数组
//合并两个数组
Object[] newArrayElement = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),
elements.length + oldElements.length);
//将两个数组中的元素拷贝到新的数组中
System.arraycopy(elements, 0, newArrayElement, 0, elements.length);
System.arraycopy(oldElements,0, newArrayElement, elements.length, oldElements.length);
//把原有的Element数组中的内容替换为新的
oldElementsFiled.set(pathList, newArrayElement);
先新建一个数组,然后把新的Element数组插入再把旧的Element数组插入,这里因为Element类是对外不可见的,因此需要使用Array.newInstance
来创建对应的数组。
2.5 最后用组装的新Element数组替换旧的数组
//把原有的Element数组中的内容替换为新的
oldElementsFiled.set(pathList, newArrayElement);
3. 制作dex文件
先使用jar命令把指定的class文件打成jar包jar cvf fix.jar com/example/hotfix/Bug.class
使用dx命令(build-tools/29.0.3/目录下)把刚才的jar打包成dex文件 dx --dex --output=output.dex --fix.jar
然后把这个dex文件push到手机目录下,在修复时把这个文件传入就可以加载对应的Class。