1.原理
JAVA虚拟机JVM在运行时,加载的是.classes的字节码文件。Android有自己的虚拟机Dalvik/ART虚拟机,他们加载的是dex文件,但是他们的工作原理都一样,都是经过ClassLoader类加载器。Android在ClassLoader的基础上又定义类DexClassLoader用来加载jar、apk、dex文件、加载jar、apk也是最终抽取里面的Dex文件进行加载.
2.实例
(1)核心类:FixDexUtil
package com.zw.hotrepairapp;
import android.content.Context;
import android.os.Environment;
import android.widget.Toast;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashSet;
import androidx.annotation.NonNull;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public class FixDexUtil {
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";
public static final String DEX_DIR = "odex";
private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
private static HashSet<File> loadedDex = new HashSet<>();
static {
loadedDex.clear();
}
/**
* 加载补丁,使用默认目录: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) {
// dex合并之前的dex
doDexInject(context, loadedDex);
}
/**
* @author bthvi
* @time 2018/6/25 0025 15:51
* @desc 验证是否需要热修复
*/
public static boolean isGoingToFix(@NonNull Context context) {
boolean canFix = false;
String path="/sdcard/hotRepair/";
// File externalStorageDirectory = Environment.getExternalStorageDirectory();
File externalStorageDirectory = new File(path);
// 遍历所有的修复dex , 因为可能是多个dex修复包
File fileDir = externalStorageDirectory != null ?
new File(externalStorageDirectory, "hotfix") :
new File(context.getFilesDir(), DEX_DIR);// data/data/包名/files/odex(这个可以任意位置)
File[] listFiles = fileDir.listFiles();
if (listFiles != null) {
for (File file : listFiles) {
if (file.getName().endsWith(DEX_SUFFIX)
|| file.getName().endsWith(APK_SUFFIX)
|| file.getName().endsWith(JAR_SUFFIX)
|| file.getName().endsWith(ZIP_SUFFIX)) {
loadedDex.add(file);// 存入集合
//有目标dex文件, 需要修复
canFix = true;
}
}
}
return canFix;
}
private static void doDexInject(Context appContext, HashSet<File> loadedDex) {
String optimizeDir = appContext.getFilesDir().getAbsolutePath() +
File.separator + OPTIMIZE_DEX_DIR;
// data/data/包名/files/optimize_dex(这个必须是自己程序下的目录)
File fopt = new File(optimizeDir);
if (!fopt.exists()) {
fopt.mkdirs();
}
try {
// 1.加载应用程序dex的Loader
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : loadedDex) {
// 2.加载指定的修复的dex文件的Loader
DexClassLoader dexLoader = new DexClassLoader(
dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录
fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁)
null,// 加载dex时需要的库
pathLoader// 父类加载器
);
// 3.开始合并
// 合并的目标是Element[],重新赋值它的值即可
/**
* BaseDexClassLoader中有 变量: DexPathList pathList
* DexPathList中有 变量 Element[] dexElements
* 依次反射即可
*/
//3.1 准备好pathList的引用
Object dexPathList = getPathList(dexLoader);
Object pathPathList = getPathList(pathLoader);
//3.2 从pathList中反射出element集合
Object leftDexElements = getDexElements(dexPathList);
Object rightDexElements = getDexElements(pathPathList);
//3.3 合并两个dex数组
Object dexElements = combineArray(leftDexElements, rightDexElements);
// 重写给PathList里面的Element[] dexElements;赋值
Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错
setField(pathList, pathList.getClass(), "dexElements", dexElements);
}
Toast.makeText(appContext, "修复完成", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 反射给对象中的属性重新赋值
*/
private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cl.getDeclaredField(field);
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 arrayLhs, Object arrayRhs) {
Class<?> clazz = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);// 得到左数组长度(补丁数组)
int j = Array.getLength(arrayRhs);// 得到原dex数组长度
int k = i + j;// 得到总数组长度(补丁数组+原dex数组)
Object result = Array.newInstance(clazz, k);// 创建一个类型为clazz,长度为k的新数组
System.arraycopy(arrayLhs, 0, result, 0, i);
System.arraycopy(arrayRhs, 0, result, i, j);
return result;
}
}
(2).需要修复的类
public class BugEntity {
String name="测试数据";
public BugEntity() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(3).调用是否修复的类
package com.zw.hotrepairapp;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import java.io.File;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class SplashActivity extends Activity {
private String[] permissions = {
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
requestPermission();
findViewById(R.id.btnConfig).setOnClickListener(view -> {
init();
Intent intent=new Intent(this,MainActivity.class);
startActivity(intent);
});
}
/**
* 申请权限
*/
public void requestPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int i = ContextCompat.checkSelfPermission(this, permissions[0]);
if (i != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, 321);
}
}
}
private void init() {
// File externalStorageDirectory = Environment.getExternalStorageDirectory();
String path="/sdcard/hotRepair/";
File externalStorageDirectory = new File(path);
Log.i("info","----"+externalStorageDirectory.getAbsolutePath());
// 遍历所有的修复dex , 因为可能是多个dex修复包
File fileDir = externalStorageDirectory != null ?
new File(externalStorageDirectory, "hotfix") :
new File(getFilesDir(), FixDexUtil.DEX_DIR);// data/user/0/包名/files/odex(这个可以任意位置)
if (!fileDir.exists()) {
fileDir.mkdirs();
}
Log.i("info","----"+fileDir.getAbsolutePath());
if (FixDexUtil.isGoingToFix(this)) {
FixDexUtil.loadFixedDex(this, Environment.getExternalStorageDirectory());
}
// new Handler().postDelayed(new Runnable() {
// @Override
// public void run() {
// startActivity(new Intent(SplashActivity.this,MainActivity.class));
// finish();
// }
// },3000);
}
}
(4).修复bug后的类
public class BugEntity {
String name="修复后的正式数据";
public BugEntity() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
3.打出热修复补丁包
(1)重新编译并拷贝出新修改的类
(2)将class文件打包成dex文件,在找到你的SDK下的build-tools\30.0.2下执行cmd ,然后执行dx --dex --no-strict --output=你要生成的dex 需要打包进dex文件的Class文件
(3)将生成的dex文件放到对应的目录
然后重启app,就可以了
4.参考 :https://blog.youkuaiyun.com/u013132758/article/details/80954639