原理:
1、PathClassLoader只能加载系统中已经安装过的apk
2、DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk
可以在Activity中打印 this.getClassLoader().toString() 输出就为dalvik.system.PathClassLoader。
查看下PathClassLoader源码,其基本都是调用父类BaseDexClassLoader构造方法,BaseDexClassLoader中一眼就看到了
private final DexPathList pathList;
这应该就是加载的dex,继续查看DexPathList类:
private Element[] dexElements;
这就是dex加载后的Element数组。
查看DexClassLoader发现其加载dex后,最终也是放入到了父类BaseDexClassLoader的private Element[] dexElements;中
所以我们可以使用DexClassLoader加载我们的补丁,再利用反射获取到PathClassLoader中的dexElements,以及DexClassLoader中的dexElements,将二者合并(补丁放前面,原来的放后面),再通过反射重新赋值给PathClassLoader中的dexElements,这样实现了热修复的效果。
实现:
1.创建一个Activity,命名SplashActivity,里面两个按钮,一个点击后实现热修复,一个跳转到主页面,记得请求WRITE_EXTERNAL_STORAGE权限。
2.主页面实现,命名为MainActivity,其中一个按钮。
3.创建一个测试类,命名为Function,添加方法test,方法中直接抛出一个异常。
4.MainActivity中的按钮点击后调用Function的test方法。
以上部分可自己实现,名字是我Demo中的名字,可自行修改。代码比较简单就不贴出来了,后面会给出完整项目。
接下来是重要的热修复部分:
/**
* 热修复工具类
*/
public class FixUtils {
private static final String DEX_SUFFIX = ".dex";
public static void fix(Context context){
// 用来存放补丁dex
File dexDir = new File(Environment.getExternalStorageDirectory() , "hotFix");
if(!dexDir.exists()){
dexDir.mkdirs();
}
// 获取到pathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
// 遍历hotFix文件夹
for (File dexFile : dexDir.listFiles()){
// 如果不是*.dex文件直接跳过
if(!dexFile.getName().endsWith(DEX_SUFFIX)){
continue;
}
// 创建DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(
dexFile.getAbsolutePath(), //dex文件路径
context.getFilesDir().getAbsolutePath(), //存放dex的解压目录(用于jar、zip、apk格式的补丁)
null, // 加载dex时需要的库
pathClassLoader.getParent()); //父类加载器
try {
// 反射获取到pathClassLoader的pathList即pathClassLoader的父类BaseDexClassLoader中的private final DexPathList pathList;
Object pathClassLoader_DexPathList = getField(pathClassLoader , Class.forName("dalvik.system.BaseDexClassLoader") , "pathList");
// 反射获取到dexClassLoader的pathList即pathClassLoader的父类BaseDexClassLoader中的private final DexPathList pathList;
Object dexClassLoader_DexPathList = getField(dexClassLoader , Class.forName("dalvik.system.BaseDexClassLoader") , "pathList");
// 反射获取到pathClassLoader中的DexPathList的dexElements
Object pathClassLoader_DexPathList_DexElements = getField(pathClassLoader_DexPathList , pathClassLoader_DexPathList.getClass() , "dexElements");
// 反射获取到dexClassLoader中的DexPathList的dexElements
Object dexClassLoader_DexPathList_DexElements = getField(dexClassLoader_DexPathList , dexClassLoader_DexPathList.getClass() , "dexElements");
// 合并两个dexElements
Object newElements = mixElements(pathClassLoader_DexPathList_DexElements , dexClassLoader_DexPathList_DexElements);
// 重新获取pathClassLoader的pathList,重复使用之前的可能会报错
Object pathClassLoader_DexPathList1 = getField(pathClassLoader , Class.forName("dalvik.system.BaseDexClassLoader") , "pathList");
// 将上面合并的dexDlements重新赋值给pathClassLoader的pathList的dexElements
setField(pathClassLoader_DexPathList1 , pathClassLoader_DexPathList1.getClass() , "dexElements" , newElements);
Toast.makeText(context, "修复完成", Toast.LENGTH_SHORT).show();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
/**
* 反射获取属性值
* @param object 需要获取值的对象
* @param clazz 需要获取值所在的类
* @param fieldName 属性名
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getField(Object object , Class clazz , String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}
/**
* 反射为对象的属性赋值
* @param object 需要赋值的对象
* @param clazz 需要赋值的属性所在的类
* @param fieldName 属性名
* @param value 属性值
* @throws NoSuchFieldException
* @throws IllegalAccessException
* @throws ClassNotFoundException
*/
private static void setField(Object object , Class clazz ,String fieldName ,Object value) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object , value);
}
/**
* 反射合并两个Element数组
* @param element1
* @param element2
* @return
*/
private static Object mixElements(Object element1 , Object element2){
int len1 = Array.getLength(element1);
int len2 = Array.getLength(element2);
int newSize = len1 + len2;
Class clazz = element1.getClass().getComponentType();
Object result = Array.newInstance(clazz , newSize);
// 先添加补丁
System.arraycopy(element2 , 0 , result , 0 , len2);
// 后添加原dex
System.arraycopy(element1 , 0 , result , len2 , len1);
return result;
}
}
SplashActivity中的修复按钮调用
FixUtils.fix(this);
准备补丁文件:
1.注释掉Function类test方法中抛出的异常,更改为打印个Log。
2.去掉红框中的√ 这样每次都会重新编译,有√的时候每次只会重新编译更改过的部分
3.clean
4.rebuild
5.获取到编译好的class文件,删除不需要的class文件,只留下Function.class。其他Gradle版本,class位置可能有所变化,自己可以在build目录里找下
6.将class编译为dex文件
dx文件在sdk\build-tools\版本中。使用方法 dx --dex --output=输出目录 class目录
例如dx --dex --output=d:/classes2.dex d:/dex
这里的class目录是完成目录,是需要包名的,比如我的demo中就需要