Android热修复(Hot Fix)案例全剖析(二)

    在上篇博客中,我们初步了解了Android热修复的基本流程,具体可以看我的博客Android热修复(Hot Fix)案例全剖析(一),那么本篇博客,我将为大家全面剖析Android热修复的实现案例。

1.将下载的修复补丁拷贝到应用的内部缓存目录中

    在上一篇文章中,我们已经生成了用于修复Bug的classes2.dex补丁包,通常我们会在APP后台子线程中自动调用热修复接口,并下载修复补丁,这里为了方便演示,我们把已经下载好的dex补丁文件放到SD卡中,然后将下载的修复补丁拷贝到应用的内部缓存目录中cacheDir,之所以这样做是因为下一步我们需要使用类加载器ClassLoader在内部缓存中加载classese.dex包。下面是我写的一个将classes2.dex包拷贝到内部缓存目录中的方法。

/**
 * 修复方法
 */
private void castielFixMethod() {
        // 创建一个内部缓存目录,把我们SD卡中的"classes2.dex"文件拷贝到内部缓存目录中cache
        File fileSDir = getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE);
        String name = "classes2.dex";
        String filePath = fileSDir.getAbsolutePath() + File.separator + name;
        File file = new File(filePath);
        if (file.exists()) {// 判断是否已经存在dex文件
            Log.i("WY", "已经存在dex文件");
            file.delete();
        }
        // 通过IO流将dex文件写到我们的缓存目录中去
        InputStream is = null;
        FileOutputStream fos = null;
        // 版权所有,未经许可请勿转载:猴子搬来的救兵http://blog.csdn.net/mynameishuangshuai
        try {
            is = new FileInputStream(Environment.getExternalStorageDirectory());
            fos = new FileOutputStream(filePath);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
            File f = new File(filePath);
            Log.i("WY", "filePath:" + f.getAbsolutePath());
            if (f.exists()) {
                Toast.makeText(this, "新的dex文件已经覆盖", Toast.LENGTH_LONG).show();
            }
            // 动态加载修复dex包 
            FixDexUtils.loadFixedDex(this);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                fos.close();
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

2.实现热修复工具类

这里首先给大家普及一下类加载的原理:
    在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类。由于Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。
    ClassLoader去加载Dex文件,首先Dex文件是放在/data/apk/packagename~1/base/apk,由于apk是一个类似于压缩包的东西,Android其实是使用一个优化的临时缓存目录optimizeDir(dex),专门把Dex文件解压进去,这样以后就从这个临时缓存目录中加载,提高效率。
    在1代码中我们提到了loadFixedDex()方法,便是我们的核心热修复工具类,我给大家具体讲一下:
    ClassLoader有一个简单的实现类-PathClassLoader。该类作为Android的默认的类加载器,本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法,该方法是ClassLoader的核心。
    每个ClassLoader有一个pathList变量,是标识dex文件的路径,我们通过该路径加载dex文件,默认不分包的时候只有一个dex文件,当然谷歌在顶层设计时允许我们有多个dex文件。
    ClassLoader去找optimizeDir(dex)目录,然后把目录添加到pathList里面去,接着去找目录下面的所有的dex文件,把这些dex文件当做一个数组放到dexElements中去,这样就可以有多个dex文件。

pathList{
    dexElements{
        [classes.dex,classes2.dex]
    }
}

    ClassLoader每加载一个类,它会先找classes.dex,如果找不到就去classes2.dex中找,如果里面又一个dex有问题,比如说classes2.dex出问题了,我们就需要弄一个修复的新的classes2.dex文件放到数组中去,替换掉有问题的;但是classes2.dex中可能有多个类,除了有问题的类,也可能有很多正确的类,我们在替换时没必要把所有的类都替换掉,所以我们只要替换有问题的类。
    为此,我们可以采用一个策略,把新的替换的dex文件放到数组的最前面,最终数组的形态为:

[classes2.dex,classes.dex,classes2.dex]

    这里解释下,ClassLoader类加载器先加载我们修复的正确的dex文件,然后顺序加载数组中其他的dex元素,到了最后加载到旧的classes2.dex元素,由于前面已经加载了更新的classes2.dex(更新的dex文件中只包含修复的class),那么旧的classes2.dex元素中的有Bug的class就不会再加载,而是只加载其余的没有错误的class。
    整个流程其实非常简单,但是如果我们要实现这个过程却有个障碍,那就是由于我们的APK程序可能正在运行,谷歌并没有提供相关的接口方法去实现这一步骤,为此,我们需要使用反射的手段去实现。
1.首先需要反射ClassLoader类,找到里面的pathList变量,然后找到dexElements[]数组,该数组在修复之前只有两个元素,分别是classes.dex和classes2.dex(出错的),假设值数组1;
2.接着我们要往dexElements[]数组中添加classes2.dex文件。
Android中要想实现加载dex文件,需要使用DexClassLoader类加载classes2.dex(补丁),加载到dexElements[]数组中去,假设值数组2。
3.最后,我们需要把两个dexElements[]数组合并,作为一个新数组dexElements[],该数组中包含元素为classes2.dex(补丁),classes.dex和classes2.dex(出错的),完成后将数组返回赋值给系统的ClassLoader。

最后贴出热修复工具类源码

public class FixDexUtils {

    private static HashSet<File> loadedDex = new HashSet<File>();

    public static void loadFixedDex(Context context) {
        if (context == null) {
            return;
        }
        // 首先拿到缓存目录
        File fileSDir = context.getDir(MyConstants.DEX_DIR,
                Context.MODE_PRIVATE);
        File[] listFils = fileSDir.listFiles();
        // 遍历缓存文件
        for (File file : listFils) {
            // 如果文件是以"classes"开始或者以".dex"结尾,说明这是从SDK中拷贝回来的修复包
            if (file.getName().startsWith("classes")
                    || file.getName().endsWith(".dex")) {
                Log.i("WY", "当前dexName:" + file.getName());
                loadedDex.add(file);
            }
        }
        doDexInject(context, fileSDir);
    }

    private static void doDexInject(Context context, File fileDir) {
        if (Build.VERSION.SDK_INT >= 23) {
            Log.i("WY", "Unable to do dex inject on SDK"
                    + Build.VERSION.SDK_INT);
        }
        // .dex 的加载需要一个临时目录
        String optimizeDir = fileDir.getAbsolutePath() + File.separator
                + "opt_dex";
        File fopt = new File(optimizeDir);
        if (!fopt.exists())
            fopt.mkdirs();
        try {
            // 根据.dex 文件创建对应的DexClassLoader 类
            for (File file : loadedDex) {// 循环迭代,用于多个修复包同时注入
                DexClassLoader classLoader = new DexClassLoader(
                        file.getAbsolutePath(), fopt.getAbsolutePath(), null,
                        context.getClassLoader());
                // 注入
                inject(classLoader, context);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void inject(DexClassLoader classLoader, Context context) {

        // 获取到系统的DexClassLoader 类
        PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
        try {
            Object dexElements = combineArray(
                    getDexElements(getPathList(classLoader)),
                    getDexElements(getPathList(pathLoader)));
            Object pathList = getPathList(pathLoader);
            setField(pathList, pathList.getClass(), "dexElements", dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 通过反射获取DexPathList中dexElements
     */
    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException,
            IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }

    /**
     * 通过反射获取BaseDexClassLoader中的PathList对象
     */
    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException,
            IllegalAccessException, ClassNotFoundException {
        return getField(baseDexClassLoader,
                Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 通过反射获取指定字段的值
     */
    private static Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

    /**
     * 通过反射设置字段值
     */
    private static void setField(Object obj, Class<?> cl, String field,
            Object value) throws NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {

        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    /**
     * 合并两个数组
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, 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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章