什么是热修复
当线上应用出现bug时,无需用户安装,推送补丁到用户端无感知修复bug,节省用户流量提高用户使用体验,修复准确率高
-
热修复的原理
class类只会被ClasssLoader加载一次,Dalvik/ART加载dex文件。通过类加载器(ClassLoader)加载代码替换相关的代码。
-
热修复分为两种:
- 在Native层替换方法表中的方法,直接在虚拟机的方法区实现替换
- 在Java层实现热修复
前者是替换后立即生效,后者是需要重启App生效
-
Java层的热修复为什么需要重启App才能生效
Java的懒加载机制,在App不重新启动时,新类不能替换老的类。Class只能被ClassLoader加载一次,App启动后字节码文件已经被全部加载到虚拟机,所以Java层的热修复重新启动了App才会生效。
热修复的不足
所有的热修复不能保证100%修复成功
64K问题
Android虚拟机可执行的字节码单个dex文件内可引用的方法数量最大限制是65536,超过这个数量就会爆出异常。
解决方法是将应用构建流程配置为多个dex包
Dalvik 可执行文件分包配置
1.将 Gradle 构建配置更改为启用 Dalvik 可执行文件分包
2.修改清单文件以引用 MultiDexApplication 类
multiDexEnabled true
规避64K限制
合理的使用方法数量资源;在gradle中配置使用代码混淆和代码自检,去除无用的代码方法
Java层手写Android热修复
-
创建一个BugClass类
package com.example.firstapplication.hotfix;
import android.content.Context;
import android.widget.Toast;
/**
* Create by pengQun 2021/3/3
* Desc:创建一个bug类
*/
public class BugClass {
public static void bugClass(Context context) {
Toast.makeText(context, context.getPackageName() + ",这是一个Bug...", Toast.LENGTH_SHORT).show();
}
}
-
热修复的核心工具类
package com.example.firstapplication.hotfix;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
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;
/**
* Create by pengQun
* Desc:热修复的核心工具类
*/
public class FixDexUtil {
private static final String TAG = FixDexUtil.class.getSimpleName();
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();
}
public static void loadFixedDex(Context context) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
loadFixedDex(context, null);
}
/**
* 加载补丁
*
* @param context 上下文
* @param patchFileDir 补丁所在的目录
*/
public static void loadFixedDex(Context context, File patchFileDir) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
//合并dex(补丁dex合并现有的dex)
doInjectDex(context, loadedDex);
}
/**
* 验证是否需要热修复
*
* @param context context
*/
public static boolean isGoingToFix(@NonNull Context context) {
boolean canFix = false;
File externalFilesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
String downPath = externalFilesDir.getAbsolutePath();
String path007 = downPath + File.separator + "007";
Log.d(TAG, "path007: " + path007);
// String pathDex = downPath + File.separator + DEX_DIR;
File fileDir = new File(path007);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
File[] listFiles = fileDir.listFiles();
for (File listFile : listFiles) {
String fileName = listFile.getName();
if (fileName.startsWith("classes")
&& (fileName.endsWith(DEX_SUFFIX) ||
fileName.endsWith(JAR_SUFFIX) ||
fileName.endsWith(ZIP_SUFFIX) ||
fileName.endsWith(APK_SUFFIX))) {
loadedDex.add(listFile);
canFix = true;
}
}
return canFix;
}
private static void doInjectDex(Context context, HashSet<File> hashSet) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
String optimizeDir = context.getFilesDir().getAbsolutePath()
+ File.separator + OPTIMIZE_DEX_DIR;
Log.d(TAG, "------> optimizeDir: " + optimizeDir);
//存放dex的解压目录
File file = new File(optimizeDir);
if (!file.exists()) {
file.mkdirs();
}
//1.加载应用程序dex的classLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//遍历修复的dex文件
for (File dexFile : hashSet) {
//2.加载指定修复dex的classLoader(补丁的classLoader)
DexClassLoader dexClassLoader = new DexClassLoader(
dexFile.getAbsolutePath(),//修复补丁所在的目录
file.getAbsolutePath(),//补丁的解压目录(用于jar,zip,zpk格式的补丁)
null,//加载dex时需要的库
pathClassLoader//父类加载器
);
//3.获取dex,开始合并 合并的目标是Element[]
// BaseDexClassLoader中有变量:DexPathList pathList
// DexPathList中有变量:Element[] dexElements
//3.1获取pathList
//获取补丁中的pathList
Object dexPathList = getPathList(dexClassLoader);
//获取当前apk的dex中的pathList
Object apkPathList = getPathList(pathClassLoader);
//3.2反射获取element数组
//获取补丁中的element数组
Object dexElements = getDexElements(dexPathList);
//获取apk的dex中的element数组
Object apkDexElements = getDexElements(apkPathList);
//4.合并Element数组
Object combineArray = combineArray(dexElements, apkDexElements);
//5.重新给PathList里边的Element[] dexElements 赋值
//一定要重新获取,不要用上面的apkPathList,会报错
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", combineArray);
}
Toast.makeText(context, "修复完成", Toast.LENGTH_SHORT).show();
}
/**
* 反射给对象中的属性赋值
*/
private static void setField(Object obj, Class<?> aClass, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = aClass.getDeclaredField(fieldName);
declaredField.setAccessible(true);
declaredField.set(obj, value);
}
/**
* 反射得到对象中的属性值
*
* @param obj
* @param aClass
* @param fieldName
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getField(Object obj, Class<?> aClass, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = aClass.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
/**
* 反射得到类加载器中的pathList对象
*
* @param baseDexClassLoader
* @return
*/
private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Class<?> aClass = Class.forName("dalvik.system.BaseDexClassLoader");
return getField(baseDexClassLoader, aClass, "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<?> componentType = arrayLhs.getClass().getComponentType();
//得到左边数组的长度(补丁数组)
int i = Array.getLength(arrayLhs);
//得到原数组dex的长度
int j = Array.getLength(arrayRhs);
//数组的总长度
int length = i + j;
//创建一个类型为class的数组
Object instance = Array.newInstance(componentType, length);
//复制数组到目标数组
System.arraycopy(arrayLhs, 0, instance, 0, i);
System.arraycopy(arrayRhs, 0, instance, i, j);
return instance;
}
}
运行代码点击按钮调用 BugClass.bugClass() 方法弹出bug提示
将BugClass代码修正,然后Build-----> ReBuild Project重新构建项目生成字节码文件。如下:
public class BugClass {
public static void bugClass(Context context) {
Toast.makeText(context, "bug已经修复", Toast.LENGTH_SHORT).show();
}
}
复制ReBuild Project后的BugClass.class字节码文件,路径如下:
hotfix.png
-
将BugClass.class文件使用dex工具打包成dex文件
dex打包,可参考我的另一篇博文
mac下dex打包
-
将生成好的BugClass.dex文件拷贝放到手机的文件路径下(/storage/emulated/0/Android/data/com.example.firstapplication/files/Download/007)【该路径要与代码中的对应】,然后重启App
效果
看下修复前和修复后的效果
fix_before.jpg
fix_after.jpg
总结
java层热修复是利用classLoader只能加载一次class类到Dalvik/ART虚拟机执行的特点,将修复后的.class文件打包成.dex文件在问题代码前预先加载到虚拟机,以此达到热修复的目的。(在dex的Element数组合并的时候就能看出来,补丁的Element数组要放置在apk的Element的前边,ClassLoader加载了补丁的class就不会再去加载apk中的class)
网友评论