前言
最近一段时间看了一些关于Android热修复的知识,比如Andfix,Tinker,Sophix等,看了这些框架的原理,就想着自己能不能手撸一个简单的demo。下面我们就来自己动手实现Android热修复吧。
热修复实现原理
所谓热修复就是,在我们应用上线后出现小bug需要及时修复时,不用再发新的安装包,只需要发布补丁包,在客户不知不觉之间修复掉bug,JAVA虚拟机JVM在运行时,加载的是.classes的字节码文件。而Android也有自己的虚拟机Dalvik/ART虚拟机,不过他们加载的是dex文件,但是他们的工作原理都一样,都是经过ClassLoader类加载器。Android在ClassLoader的基础上又定义类PathClassLoader和DexClassLoader,两者都继承自BaseDexClassLoader,下面我们看下他们间的
区别:
BaseDexClassLoader源代码位于libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java。
PathClassLoader源代码位于libcore\dalvik\src\main\Java\dalvik\system\PathClassLoader.java。他主要用来加载系统类和应用类。
DexClassLoader源代码位于libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java。用来加载jar、apk、dex文件.加载jar、apk也是最终抽取里面的Dex文件进行加载.

Android热修复目前各大厂商都有自己的热修复工具,主要分为两大主流以阿里为代表的Native层替换方法表中的方法实现热修复[AndFix ,Sophix等],和以腾讯美团为代表的在JAVA层实现热修复[Tinker,Robust等]。后者要实现热修复必须要重启APP,而前者则不需要重启APP,直接在虚拟机的方法区实现方法替换。
本文主要实现的是在JAVA层实现热修复,也就是重启APP才能生效。
手写Android热修复框架
下面我们一步一步来实现Android热修复。
写一个专门带有bug的类
既然要测试热修复,我们肯定要写一个带有bug的类。
下面我们要写一个热修复的核心工具类。
热修复核心类
package com.example.bthvi.mycloassloaderapplication;
import android.content.Context;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.widget.Toast;
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 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();
}
public static void loadFixedDex(Context context) {
loadFixedDex(context, null);
}
public static void loadFixedDex(Context context, File patchFilesDir) {
doDexInject(context, loadedDex);
}
public static boolean isGoingToFix(@NonNull Context context) {
boolean canFix = false;
File externalStorageDirectory = Environment.getExternalStorageDirectory();
File fileDir = externalStorageDirectory != null ?
new File(externalStorageDirectory,"007"):
new File(context.getFilesDir(), DEX_DIR);
File[] listFiles = fileDir.listFiles();
if (listFiles != null){
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);
canFix = true;
}
}
}
return canFix;
}
private static void doDexInject(Context appContext, HashSet<File> loadedDex) {
String optimizeDir = appContext.getFilesDir().getAbsolutePath() +
File.separator + OPTIMIZE_DEX_DIR;
File fopt = new File(optimizeDir);
if (!fopt.exists()) {
fopt.mkdirs();
}
try {
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : loadedDex) {
DexClassLoader dexLoader = new DexClassLoader(
dex.getAbsolutePath(),
fopt.getAbsolutePath(),
null,
pathLoader
);
Object dexPathList = getPathList(dexLoader);
Object pathPathList = getPathList(pathLoader);
Object leftDexElements = getDexElements(dexPathList);
Object rightDexElements = getDexElements(pathPathList);
Object dexElements = combineArray(leftDexElements, rightDexElements);
Object pathList = getPathList(pathLoader);
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);
}
private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
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);
int k = i + j;
Object result = Array.newInstance(clazz, k);
System.arraycopy(arrayLhs, 0, result, 0, i);
System.arraycopy(arrayRhs, 0, result, i, j);
return result;
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
我们这里暂且指定热修复目录007,下面我们看一下如何调用。
Splash页面调用检查热修复
下面我们先来看下有bug时的APP。
在出bug的对应类修复bug
修改好bug之后我们需要打出补丁包。
打出热修复补丁包
在AndroidStudio里面关闭掉Instant_Run
由于Android Studio的instan run的原理也是热修复,所以安装的时候不会安装完整的安装包,只会安装新改变的代码。
重新编译并拷贝出新修改的类
首先点击Build->RebuildProject来重新构建,构建完成之后,可以在app/build/interintermediate/debug/包名/找到你刚刚修改的class文件,将他拷贝出来,要连同包名路径一起拷贝出来。
将class文件打包成dex文件
我们前面知道热修复的原理是Dalvik/ART加载dex文件,所以接下来我们要将class文件打包成dex文件,首先我们找到AndroidSDK的build-tools 目录下,在控制台下进入该目录下的任意一个版本,执行dx命令,关于dx命令的使用帮助可以使用dx -- help,下面们通过 dx --dex [指定输出路径]/classes.dex [刚才拷贝的修复bug的类及包名的目录]这样我们就得到了.dex文件。
将打出来的dex文件放至我们指定的目录下
我们将打出来的dex文件放在我们指定的目录007下,当然这个目录也可以是包名。

重新启动有bug的APP
我们启动就会后发现bug已经修复了
没有修复?
很多同学反馈说没有修复,我觉得主要还是要检查下 isGoingToFix方法,因为这个方法中有一个判断是需要判断目标路径下,修复包的名称一定要为classes开头,以dex结尾
作者:紫雾凌寒
来源:CSDN