这篇文章继续我们之前写的Classloader,来说一下Android中dex插桩方式实现热修复的方案。
Android中的ClassLoader
1.实现原理
我们程序中的类在被加载的时候,最终会调用到DexPathList的findClass方法,然后在该方法中遍历Element[],获取到class返回。
我们从代码中可以得知,如果在第一个Element中找到了class,后面的就不会处理了,所以我们就应该想办法,把我们修复过的dex文件转换成Element,然后将这个Element放到的Element[]的前面,这样在加载类的时候,就会优先调用我们修复过的class,从而达到修复bug的目的。
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.实现方案
首先明确我们的目的,就是获取到Element[],然后把我们修复的dex转换为Element,再添加到Element[]的前面。
(1)Element[]在哪?
在DexPathList
(2)那DexPathList在哪?
在BaseDexClassLoader
(3)我们如何获取BaseDexClassLoader?
我们知道,我们自己的应用程序中的类是通过PathClassLoader加载,而PathClassLoader是 BaseDexClassLoader的子类,所以我们可以通过PathClassLoader获取BaseDexClassLoader,然后一步一步向上去获取,或者直接根据全类名直接反射获取。
明确了我们的方向后,我们开始按步骤实现。
2.1获取ClassLoader
首先我们获取到ClassLoader,这就很简单了 直接调用getClass().getClassLoader()就可以获取到了,或者直接通过Application、Activity等直接getClassLoader()也可以。这里我们获取到的是PathClassLoader。
2.2反射获取到DexPathList属性对象pathList
这里要注意的是,我们获取到的是PathClassLoader,而DexPathList对象是在PathClassLoader的父类(BaseDexClassLoader)中,所以我们反射调用的时候要注意,DexPathList对象BaseDexClassLoader
中的变量名是pathList,我们反射获取的时候要用到。
2.3.把补丁包patch.dex转化为Element[]
现在的问题就是我们应该如何把我们的dex文件转换成Element,我们可以通过两种方式来完成。
2.3.1通过makeDexElements方法
我们首先看PathClassLoader,它的构造方法中需要传入一个dexPath的字段,这个就是dex的路径。
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
然后我们继续跟进到它的父类中查看,也就是BaseDexClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
我们发现它将DexPathList传给了DexPathList,我们继续跟进到DexPathList。
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
...
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
...
}
最终我们发现是将dexPath传给了makeDexElements这个方法,而这个方法的返回值就是一个Element[],说明它就是我们最终需要的将dex转为Element的方法。
这里我们可以看到有一个splitDexPath方法,因为我们的dex路径不可能是一个,是按照一定规则拼接后传入的,splitDexPath就是将路径进行分割(应该是按照:分割),然后转换List<File>返回。
然后我们看makeDexElements的源码
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
...
}
files就是dex转换来的List<File>
optimizedDirectory参数是优化dex的输出路径,真正加载dex之前,会进行odex 优化。
suppressedExceptions是一个List<IOException> ,源码中直接new了一个然后传进来,我们外面调用的时候也按照这种方式。
loader是一个ClassLoader类型,我们可以直接传入我们上面获取的PathClassLoader。
isTrusted这个翻译过来就是信任的,但是我看源码中传入的都是false,这里我们也传入false。
这里在写文章阅读源码的时候发现,不同的版本makeDexElements方法的参数不一样,学习的时候用的是老版本的,没有最后的两个参数,但是28的版本中是有这两个参数的,所以可能描述和使用上有不对的地方,大家可以指出来,感激不尽,下面我们看下另外一种获取Element[]的方法。
2.3.2通过makeDexElements方法
这种方法就直接贴代码了,具体详细的代码在文末会贴出。
DexClassLoader dexClassLoader = new DexClassLoader(patch, application.getFilesDir().getAbsolutePath(),
null, classLoader);
Object pluginPathList = patchField.get(dexClassLoader);
Object[] patchElements = (Object[]) dexElements.get(pluginPathList);
2.4.把补丁包patch.dex转化的Element[]添加到系统的Element[]中(合并)
我们在通过反射获取到DexPathList中的dexElements就可以获取到系统的Element[],然后将系统的Element[]和我们修复文件的Element[]二者合并,这样就ok了。
代码很简单 我就直接贴在下面了。
修复方法
/**
* @param application 上下文 这里也可以直接传入classloader
* 因为一般我们都是在application中调用修复 所以这里写的application
* @param patch 补丁包路径
*/
public static void installPatch(Application application, String patch) {
//补丁包文件
File patchFile = new File(patch);
if (!patchFile.exists()) {
return;
}
try {
//1.获取当前应用的PathClassLoader
ClassLoader classLoader = application.getClassLoader();
//2.反射获取到DexPathList属性对象pathList
Field patchField = ShareReflectUtil.findField(classLoader, "pathList");
Object pathList = patchField.get(classLoader);
//3.1 获取pathList的dexElements属性(old)
Field dexElements = ShareReflectUtil.findField(pathList, "dexElements");
Object[] oldElements = (Object[]) dexElements.get(pathList);
//3.2 把补丁包patch.dex转化为Element[] (patch) 3.2.1和3.2.2任选其一
//3.2.1 通过makePathElements方法
// Method makePathElements = ShareReflectUtil.getMethod(pathList, "makePathElements", List.class, File.class, List.class,ClassLoader.class,boolean.class);
// List<File> files = new ArrayList<>();
// files.add(patchFile);
// File fileDir = application.getFilesDir();
// ArrayList<Exception> suppressedExceptions = new ArrayList<>();
// //如果是调用静态方法 pathList可以传null 否则必须传入对应对象
// Object[] patchElements = (Object[]) makePathElements.invoke(pathList, files, fileDir, suppressedExceptions,classLoader,false);
//3.2.2 通过DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(patch, application.getFilesDir().getAbsolutePath(),
null, classLoader);
Object pluginPathList = patchField.get(dexClassLoader);
Object[] patchElements = (Object[]) dexElements.get(pluginPathList);
//3.3 patch-old合并,并反射设置给pathList的dexElements
Object[] newElements = (Object[]) Array.newInstance(patchElements.getClass().getComponentType(),patchElements.length + oldElements.length);
System.arraycopy(patchElements,0,newElements,0,patchElements.length);
System.arraycopy(oldElements,0,newElements,patchElements.length,oldElements.length);
dexElements.set(pathList,newElements);
} catch (Exception e) {
e.printStackTrace();
}
}
反射工具类
public class ShareReflectUtil {
/**
* @param instance 对象
* @param name 属性名
* @return
*/
public static Field findField(Object instance, String name) {
Class<?> cls = instance.getClass();
while (cls != Object.class) {
try {
Field field = cls.getDeclaredField(name);
if (field != null) {
field.setAccessible(true);
return field;
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
cls = cls.getSuperclass();
}
throw new RuntimeException("no source field" + name);
}
public static Method getMethod(Object instance, String name,Class<?>... parameterTypes) {
Class<?> cls = instance.getClass();
while (cls != Object.class) {
try {
Method method = cls.getDeclaredMethod(name,parameterTypes);
if (method != null) {
method.setAccessible(true);
return method;
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
cls = cls.getSuperclass();
}
throw new RuntimeException("no source method" + name);
}
}
总结:
其实原理很简单,因为findClass方法默认是从缓存中寻找,如果缓存中没有,再去Element[]查找,因为我们将修复的dex文件插入到系统读取的Element[]数组的前面,就只会加载我们修复后的代码了。
而且初次加载后,就会加入到缓存中,之后在加载也不会在重新获取了,所以dex插桩方式进行的修复,不能即时生效,只能重启应用后才能生效,因为加载过的类不会重新在加载了。
网友评论