1.编译class文件
点Make Project
将整个包路径还有修复好的class文件复制下来。
2.class转dex
我们把自己需要修复的java文件通过AS编译成class文件之后
再用sdk目录下的dx.bat工具将class文件转成dex文件。
打开cmd,如果你设置了环境变量,可以直接在c盘调用语句,不然你就需要把路径切换到跟dx.bat一样。
比如我上面就是cd C:\Users\*****\AppData\Local\Android\Sdk\build-tools\30.0.1
把你的包放到cmd所对应的路径下,如果配置了环境变量就可以直接把包放到桌面。
回车后
dx --dex --output = com\example\hotfixdemo\classes2.dex com\example\hotfixdemo\Text.class
该命令前面对应的是生成的dex文件放置路径(相对于你现在cmd的路径)+文件名,后面的就是class文件所对应的路径(相对于你现在cmd的路径)。
3.编写热修复工具类
package com.example.hotfixdemo;
import android.content.Context;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashSet;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public class FixDexUtils {
private static final String DEX_SUFFIX = ".dex";
private static final String APK_SUFFIX = ".apk";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final HashSet<File> loadedDex = new HashSet<File>();
/**
* 加载补丁,使用默认目录:data/data/包名/files/odex
*
* @param context
*/
public static void loadFixedDex(Context context) {
loadFixedDex(context, null);
}
/**
* 加载补丁
*
* @param context 上下文
* @param patchFilesDir 补丁所在目录
*/
public static void loadFixedDex(Context context, File patchFilesDir) {
if (context == null) {
return;
}
// 遍历所有的修复dex
File fileDir = patchFilesDir != null ? patchFilesDir : new File(context.getExternalCacheDir().getAbsolutePath());// data/data/包名/cache(这个可以任意位置)
File[] listFiles = fileDir.listFiles();
for (File file : listFiles) {
if (file.getName().startsWith("classes") &&
(file.getName().endsWith(DEX_SUFFIX)
|| file.getName().endsWith(APK_SUFFIX)
|| file.getName().endsWith(JAR_SUFFIX)
|| file.getName().endsWith(ZIP_SUFFIX))) {
loadedDex.add(file);// 存入集合
}
}
// dex合并之前的dex
doDexInject(context);
}
private static void doDexInject(Context appContext) {
String optimizeDir = appContext.getFilesDir().getAbsolutePath();// data/data/包名/files (这个必须是自己程序下的目录)
File fopt = new File(optimizeDir);
if (!fopt.exists()) {
fopt.mkdirs();
}
try {
// 1.加载应用程序的dex
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : FixDexUtils.loadedDex) {
// 2.加载指定的修复的dex文件
DexClassLoader dexLoader = new DexClassLoader(
dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录
fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁)
null,// 加载dex时需要的库
pathLoader// 父类加载器
);
// 3.合并
Object dexPathList = getPathList(dexLoader);
Object pathPathList = getPathList(pathLoader);
Object leftDexElements = getDexElements(dexPathList);
Object rightDexElements = getDexElements(pathPathList);
// 合并完成
Object dexElements = combineArray(leftDexElements, rightDexElements);
// 重写给PathList里面的Element[] dexElements;赋值
Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错
setField(pathList, pathList.getClass(), dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 反射给对象中的属性重新赋值
*/
private static void setField(Object obj, Class<?> cl, Object value) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cl.getDeclaredField("dexElements");
declaredField.setAccessible(true);
declaredField.set(obj, value);
}
/**
* 反射得到对象中的属性值
*/
private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
/**
* 反射得到类加载器中的pathList对象
*/
private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 反射得到pathList中的dexElements
*/
private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
return getField(pathList, pathList.getClass(), "dexElements");
}
/**
* 数组合并
*/
private static Object combineArray(Object left, Object right) {
Class<?> componentType = left.getClass().getComponentType();
int i = Array.getLength(left);// 得到左数组长度(补丁数组)
int j = Array.getLength(right);// 得到原dex数组长度
int k = i + j;// 得到总数组长度(补丁数组+原dex数组)
Object result = Array.newInstance(componentType, k);// 创建一个类型为componentType,长度为k的新数组
System.arraycopy(left, 0, result, 0, i);
System.arraycopy(right, 0, result, i, j);
return result;
}
}
4.测试
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv1 = findViewById(R.id.tv1);
TextView tv2 = findViewById(R.id.tv2);
tv1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FixDexUtils.loadFixedDex(MainActivity.this);
}
});
tv2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, Text.getString(),Toast.LENGTH_SHORT).show();
}
});
}
}
直接看代码注释,这里我们把修复好的文件放在了外部存储的缓存文件夹中,我们可以调试手机直接把文件放进去
加载到我们的dex文件后他会存放到我们指定的另一个内部存储文件夹中。
我们主要修复的是Test类。
修复前:
public class Text {
public static String getString(){
return "出错了!";
}
}
修复后:
public class Text {
public static String getString(){
return "修复了";
}
}
这里需要注意你所修复的类被加载的时机,如果你这里修复的是MainActivity,该activity已经被加载了你再去修复是没有用的,因为该类已经被加载成一个对象存在内存中。这里我们是点了tv2才会去加载Test类。
类被加载时机:
- 定义了main的类,启动main方法时该类会被加载
- 创建类或子类的实例,即new对象的时候
- 访问类的静态方法
- 访问类的静态变量
- 反射 Class.forName()
如果我们先点击tv1再点tv2此时就会显示修复了,如果先点tv2则一直显示是出错了。