Android 代码热修复详解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 代码热修复详解相关的知识,希望对你有一定的参考价值。

java:类加载原理:
当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程,具体的加载过程如下:

1、源 ClassLoader 先判断该 Class 是否已加载,如果已加载,则直接返回 Class,如果没有则委托给父类加载器。

2、父类加载器判断是否加载过该 Class,如果已加载,则直接返回 Class,如果没有则委托给祖父类加载器。

3、依此类推,直到始祖类加载器(引用类加载器)。

4、始祖类加载器判断是否加载过该 Class,如果已加载,则直接返回 Class,如果没有则尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,则委托给始祖类加载器的子类加载器。
5、始祖类加载器的子类加载器尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,则委托给始祖类加载器的孙类加载器。

6、依此类推,直到源 ClassLoader。

7、源 ClassLoader 尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,源 ClassLoader 不会再委托其子类加载器,而是抛出异常

android类加载:
Android中的类加载器是BootClassLoader、PathClassLoader、DexClassLoader,其中BootClassLoader是虚拟机加载系统类需要用到的,PathClassLoader是App加载自身dex文件中的类用到的,DexClassLoader可以加载直接或间接包含dex文件的文件。

PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,它的一个DexPathList类型的成员变量pathList很重要。DexPathList中有一个Element类型的数组dexElements,这个数组中存放了包含dex文件(对应的是DexFile)的元素。BaseDexClassLoader加载一个类,最后调用的是DexFile的方法进行加载的

下面是代码热修复的两种方式:
1、根据类加载为父委托加载原理 将加载修复后的dex的DexClassLoader插入到PathClassLoader和BootstrapClassLoader中间,也就是将DexClassLoader设置为PathClassLoader的父加载器,将BootstrapClassLoader设置为DexClassLoader 顺序为:BootstrapClassLoader--->DexClassLoader--->PathClassLoader

//根据类加载为父委托加载原理   替换有bug的类放在dexPath中让DexClassLoader优先加载
    //类加载顺序:BootstrapClassLoader---->DexClassLoader----->PathClassLoader
    public void loadPatchDex(Context context, String dexPath, String optimizedDirectory, String librarySearchPath) {
        ClassLoader currentClassLoader = context.getClassLoader();//context加载器(PathClassLoader)
        ClassLoader parentClassLoader = currentClassLoader.getParent();//context的父加载器(BootstrapClassLoader)

        //加载dexPath加载器
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parentClassLoader);
        this.setField(ClassLoader.class, this.mParentFieldName, currentClassLoader, dexClassLoader);//设置当前类加载器的父加载器为dexClassLoader
    }

 /**
     * @param clazz
     * @param fieldName 属性名称
     * @param target    要设置属性的对象
     * @param value     设置属性的值
     * @return 设置成功
     */
    private boolean setField(Class clazz, String fieldName, Object target, Object value) {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            if (field != null) {
                field.setAccessible(true);
                field.set(target, value);
            }
            return true;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return false;//设置失败
    }

2、根据安卓的PathClassLoader加载机制将DexClassLoader加载的新的Dex添加的DexPathList的dexElements属性列表中,将更新的dex加载放在dexElements列表索引之前

     public void loadPatchDex2(Context context, String dexPath, String optimizedDirectory, String librarySearchPath) {
        ClassLoader pathClassLoader = context.getClassLoader();
        //加载dexPath加载器
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath, optimizedDirectory, librarySearchPath, pathClassLoader.getParent());
        //获取BaseDexClassLoader的DexPathList属性 私有的需要通过反射获取
        Object dexPathList1 = this.getFieldValue(BaseDexClassLoader.class, this.mDexPathListFieldName, pathClassLoader);//pathClassLoader的PathList属性
        Object dexPathList2 = this.getFieldValue(BaseDexClassLoader.class, this.mDexPathListFieldName, dexClassLoader);
        if (dexPathList1 != null && dexPathList2 != null) {
            //获取对应pathList中的dexElements属性
            Object dexElements1 = this.getFieldValue(dexPathList1.getClass(), this.mElementFieldName, dexPathList1);//当前PathClassLoader中的Element[]属性
            Object dexElements2 = this.getFieldValue(dexPathList2.getClass(), this.mElementFieldName, dexPathList2);//DexClassLoader中的Element[]属性

            if (dexElements1 != null && dexElements2 != null) {
                //将两个Element[]属性合并
                Object finalElements = combineArray(dexElements2, dexElements1);
                //将合并的值设置给当前的pathClassLoader的Element[]中
                this.setField(dexPathList1.getClass(), this.mElementFieldName, dexPathList1, finalElements);
                Log.e("HotFixEngine", "loadPatchDex2: success");
            }
        }

    }

    /**
     * 获取对应属性值
     * @param clazz
     * @param fieldName 属性名称
     * @param target 要操作的对象
     * @return
     */
    private Object getFieldValue(Class clazz, String fieldName, Object target) {
        Field field = this.getField(clazz, fieldName);
        if (field != null) {
            try {
                field.setAccessible(true);
                return field.get(target);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    private Field getField(Class clazz, String fieldName) {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 两个数组合并
     *
     * @param arrayLhs
     * @param arrayRhs
     * @return
     */
    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;
    }

Demo地址:https://github.com/xuguohongai/android/tree/master/AndroidHotFixDemo

以上是关于Android 代码热修复详解的主要内容,如果未能解决你的问题,请参考以下文章

一款超级简单易用的Android热修复框架 WandFix 使用详解

Android热更新详解

Android热修复与插件化实践之路

Android热修复原来这么简单

Android进阶笔记热修复(代码资源动态链接库)

Android热修复(HotFix)实战