Android热修复技术-自己动手实现热修复

一、基本概念

Android框架中存在一个数组,它的作用是维护全部的dex文件(我们写的类的二进制表述方式,用来给安卓虚拟机加载),安卓虚拟机会根据需要从该数组按照自上而下的顺序加载对应的类文件,即使数组中存多个同一个类对应的dex文件,虚拟机一旦找到了对应的dex文件就会停止查找,并加载。根据这个规则,我们只需要把Bug修复涉及到的类文件插入到数组的最前面去,就可以达到修复的目的。原理如下图。
在这里插入图片描述

二、实现有bug的app

在设计一个“运行”按钮,点击后执行下面会抛异常的方法。

public void doRun(View view){
    int i = 10 / 0;
    Log.d("tag", "doRun>>result:" + i);
}

运行后会抛如下异常,程序会崩溃。
在这里插入图片描述

三、修复bug

上面异常是因为分母是0,所以我们把上面代码改成如下:

public void doRun(View view) {
    int i = 10 / 1;
    Log.d("tag", "doRun>>result:" + i);
}

然后生成class文件。将com复制到桌面的patch文件夹中。
在这里插入图片描述然后删除除了MainActivity.class外的其他文件。执行如下命令生成dex文件。

dx --dex --output patch.dex C:\Users\wangheng\Desktop\patch

这样就生成了补丁dex文件。然后将dex文件push进设备中。

四、加载补丁文件

加载补丁文件调用startFix()方法。流程就是获取程序的PathClassLoader (安装到Android中的dex)和通过dex文件的路径新建DexClassLoader(补丁dex);然后分别获取他们的dexpathlist;然后分别获取dexElements 数组(包含了所有dex文件);然后合并两个dexElements 数组,注意补丁dex的dexElements 数组放在前面(即索引更小);然后将合并的dexElements 数组设置到pathClassLoader 。以后加载类就从合并的dexElements 数组中加载,因为补丁类的dexElements 数组放在前面,先从补丁 dexElements 中加载,加载成功就不会去加载bug类了,因此bug被修复了。代码如下:

// 加载补丁文件
public static void startFix(Context context, String patchPath) {
    if (context == null) {
        return;
    }

    Log.d("tag", "startFix>>patchPath:" + patchPath);

    File patchFileInApp = context.getDir(Constant.PATCH_DIR_NAME_IN_APP, Context.MODE_PRIVATE);
    if (!patchFileInApp.exists()) {
        patchFileInApp.mkdirs();
    }

    //获取应用内部的类加载器
    PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
    //实例化dexClassLoader用于加载补丁dex
    DexClassLoader dexClassLoader = new DexClassLoader(patchPath, patchFileInApp.getAbsolutePath(), null, pathClassLoader);

    try {
        //获取dexclassloader和pathclassloader的dexpathlist
        Object dexPathList = getPathList(dexClassLoader);
        Object pathPathList = getPathList(pathClassLoader);
        //获取补丁的elements数组
        Object dexElements = getDexElements(dexPathList);
        //获取程序的elements
        Object pathElements = getDexElements(pathPathList);
        //合并两个数组
        Object resultElements = combineArray(dexElements, pathElements);
        //将合并后的数组设置给PathClassLoader
        setField(pathPathList, pathPathList.getClass(), "dexElements", resultElements);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

// 通过反射设置属性的值
private static void setField(Object pathPathList, Class<?> clazz, String fieldName, Object resultElements) {
    try {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(pathPathList, resultElements);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

// 合并两个数组
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
    Class<?> clazz = arrayLhs.getClass().getComponentType();
    int i = Array.getLength(arrayLhs);
    int j = Array.getLength(arrayRhs) + i;
    Object result = Array.newInstance(clazz, j);
    for (int k = 0; k < j; ++k) {
        if (k < i) {
            Array.set(result, k, Array.get(arrayLhs, k));
        } else {
            Array.set(result, k, Array.get(arrayRhs, k - i));
        }
    }
    return result;
}

//获得dexElements
private static Object getDexElements(Object dexPathList) {
    return getField(dexPathList, dexPathList.getClass(), "dexElements");
}

//获得DexPathList
private static Object getPathList(Object classLoader) throws ClassNotFoundException {
    return getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}

//通过反射获取一个类私有属性的值
private static Object getField(Object obj, Class<?> clazz, String fieldName) {
    try {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return null;
}

注意:调用加载补丁文件的方法应该在Application中调用,在其他类还没有使用的时候就调用。如果先使用bug类,使用bug类时就会去加载bug类,一旦加载后就不会再加载了,也就是说补丁dex中修复bug类的就不会加载了,就达不到修复bug的目的。调用代码如下:

public class MyApp extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch.dex";
        HotFixUtil.startFix(this, patchPath);
        Log.d("tag", "app attachBaseContext");
    }
}

现在我们运行代码,点击运行时就不会抛异常了,bug被修复了。
源码下载请点击


版权声明:本文为u011682673原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。