Android热修复原理及实现

原理:

1、PathClassLoader只能加载系统中已经安装过的apk

2、DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk

可以在Activity中打印 this.getClassLoader().toString() 输出就为dalvik.system.PathClassLoader。

查看下PathClassLoader源码,其基本都是调用父类BaseDexClassLoader构造方法,BaseDexClassLoader中一眼就看到了

private final DexPathList pathList;

这应该就是加载的dex,继续查看DexPathList类:

private Element[] dexElements;

这就是dex加载后的Element数组。

查看DexClassLoader发现其加载dex后,最终也是放入到了父类BaseDexClassLoader的private Element[] dexElements;中

所以我们可以使用DexClassLoader加载我们的补丁,再利用反射获取到PathClassLoader中的dexElements,以及DexClassLoader中的dexElements,将二者合并(补丁放前面,原来的放后面),再通过反射重新赋值给PathClassLoader中的dexElements,这样实现了热修复的效果。

实现:

1.创建一个Activity,命名SplashActivity,里面两个按钮,一个点击后实现热修复,一个跳转到主页面,记得请求WRITE_EXTERNAL_STORAGE权限。

2.主页面实现,命名为MainActivity,其中一个按钮。

3.创建一个测试类,命名为Function,添加方法test,方法中直接抛出一个异常。

4.MainActivity中的按钮点击后调用Function的test方法。

以上部分可自己实现,名字是我Demo中的名字,可自行修改。代码比较简单就不贴出来了,后面会给出完整项目。

接下来是重要的热修复部分:

/**
 * 热修复工具类
 */
public class FixUtils {
    private static final String DEX_SUFFIX = ".dex";
    public static void fix(Context context){
//        用来存放补丁dex
        File dexDir = new File(Environment.getExternalStorageDirectory() , "hotFix");
        if(!dexDir.exists()){
            dexDir.mkdirs();
        }

//          获取到pathClassLoader
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//        遍历hotFix文件夹
        for (File dexFile : dexDir.listFiles()){
//            如果不是*.dex文件直接跳过
            if(!dexFile.getName().endsWith(DEX_SUFFIX)){
               continue;
            }
//            创建DexClassLoader
            DexClassLoader dexClassLoader = new DexClassLoader(
                    dexFile.getAbsolutePath(),               //dex文件路径
                    context.getFilesDir().getAbsolutePath(), //存放dex的解压目录(用于jar、zip、apk格式的补丁)
                    null,                  // 加载dex时需要的库
                    pathClassLoader.getParent());            //父类加载器

            try {
//                反射获取到pathClassLoader的pathList即pathClassLoader的父类BaseDexClassLoader中的private final DexPathList pathList;
                Object pathClassLoader_DexPathList = getField(pathClassLoader , Class.forName("dalvik.system.BaseDexClassLoader") , "pathList");
//               反射获取到dexClassLoader的pathList即pathClassLoader的父类BaseDexClassLoader中的private final DexPathList pathList;
                Object dexClassLoader_DexPathList = getField(dexClassLoader , Class.forName("dalvik.system.BaseDexClassLoader") , "pathList");
//                反射获取到pathClassLoader中的DexPathList的dexElements
                Object pathClassLoader_DexPathList_DexElements = getField(pathClassLoader_DexPathList , pathClassLoader_DexPathList.getClass() , "dexElements");
//                反射获取到dexClassLoader中的DexPathList的dexElements
                Object dexClassLoader_DexPathList_DexElements = getField(dexClassLoader_DexPathList , dexClassLoader_DexPathList.getClass() , "dexElements");
//                合并两个dexElements
                Object newElements = mixElements(pathClassLoader_DexPathList_DexElements , dexClassLoader_DexPathList_DexElements);
//                重新获取pathClassLoader的pathList,重复使用之前的可能会报错
                Object pathClassLoader_DexPathList1 = getField(pathClassLoader , Class.forName("dalvik.system.BaseDexClassLoader") , "pathList");
//                将上面合并的dexDlements重新赋值给pathClassLoader的pathList的dexElements
                setField(pathClassLoader_DexPathList1 , pathClassLoader_DexPathList1.getClass() , "dexElements" , newElements);

                Toast.makeText(context, "修复完成", Toast.LENGTH_SHORT).show();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }

        }
    }

    /**
     * 反射获取属性值
     * @param object 需要获取值的对象
     * @param clazz  需要获取值所在的类
     * @param fieldName 属性名
     * @return
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private static Object getField(Object object , Class clazz , String fieldName) throws NoSuchFieldException, IllegalAccessException {
            Field field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(object);
    }

    /**
     * 反射为对象的属性赋值
     * @param object 需要赋值的对象
     * @param clazz  需要赋值的属性所在的类
     * @param fieldName 属性名
     * @param value 属性值
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     * @throws ClassNotFoundException
     */
    private static void setField(Object object , Class clazz ,String fieldName ,Object value) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object , value);
    }

    /**
     * 反射合并两个Element数组
     * @param element1
     * @param element2
     * @return
     */
    private static Object mixElements(Object element1 , Object element2){
        int len1 = Array.getLength(element1);
        int len2 = Array.getLength(element2);
        int newSize = len1 + len2;

        Class clazz = element1.getClass().getComponentType();
        Object result = Array.newInstance(clazz , newSize);
//        先添加补丁
        System.arraycopy(element2 , 0 , result , 0  , len2);
//        后添加原dex
        System.arraycopy(element1 , 0 , result , len2  , len1);
        return result;

    }


}

SplashActivity中的修复按钮调用

FixUtils.fix(this);

准备补丁文件:

1.注释掉Function类test方法中抛出的异常,更改为打印个Log。

2.去掉红框中的√ 这样每次都会重新编译,有√的时候每次只会重新编译更改过的部分

3.clean

4.rebuild

5.获取到编译好的class文件,删除不需要的class文件,只留下Function.class。其他Gradle版本,class位置可能有所变化,自己可以在build目录里找下

6.将class编译为dex文件

dx文件在sdk\build-tools\版本中。使用方法 dx --dex --output=输出目录 class目录

例如dx --dex --output=d:/classes2.dex d:/dex

这里的class目录是完成目录,是需要包名的,比如我的demo中就需要

将获取到的dex文件放到定义的文件夹中,demo中是hotFix文件夹,没有可以自己创建。

进入app后,点击SplashActivity修复按钮,再进入MainActivity,点击按钮不会崩溃,并打印出了Log。

完整项目地址https://github.com/Duzilin1994/hotFix

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值