Android通过dex插桩进行热修复全流程原理简述

该文章已生成可运行项目,

文章参考:

android热修复实现

Android Dex文件详解

主流热修复方案

腾讯:Tinder
饿了么:Amigo

阿里:AndFix(已停止维护)

上面两个是类加载方案,即Dex插桩;阿里是底层替换方案


Dex插桩方案

原理:类加载

当我们需要热修复的时候,最常见的情况是想短时间内更改应用内的几个.java文件,但是打包apk下载安装流程太长,因此就需要一个更好的解决方案。而为了更好的解决方案,我们需要先了解我们的代码最终到了哪。

众所周知,java代码一般会编译成.class文件运行在JVM虚拟机上,但是Android在4.4之前用DVM虚拟机,DVM虚拟机不能直接运行java字节码,只能运行.dex文件,4.4之后ART虚拟机也是运行的.dex文件。对于Android最终安装的apk,也是把dex文件、资源文件和AndroidManifest文件一起打包在一起再进行签名。所以,我们如果需要更新apk,主要就是想更新里头的.dex文件。


而了解代码到了哪后,我们还需要了解代码怎么加载的。

首先在java中,classLoader通过双亲委派模型实现了类加载,ClassLoader在loadClass中,先判断自己又没有加载过这个class文件,没有的话让parent尝试loadClass,如果parent没有再通过findClass进行加载。

不同的层次ClassLoader负责加载不同范围的类,从最基本的Java类到应用程序类。
这样的层次结构允许类的隔离、版本管理和自定义加载,保护了类之间的互相影响

  • 对于基本的Java类,android用的BootClassLoader,JVM用的BootstrapClassLoader;
  • 对于指定目录的.class文件,android用的DexClassLoader,JVM用的CustomClassLoader;
  • 对于系统中已经安装的应用也就是/data/app下的.class文件,android用的PathClassLoader,类似于Java的AppClassLoader。
  • 除此之外还有个BaseDexClassLoader,是DexClassLoader和PathClassLoader的父类

其中,DexClassLoader和PathClassLoader都没有实现findClass方法,只有父类BaseDexClassLoader里头有实现。

    private final DexPathList pathList;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        // 实质是通过pathList的对象findClass()方法来获取class
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

通过源码我们能够得知,findClass最后是通过DexPathList实现

    public Class findClass(String name, List&lt;Throwable&gt; suppressed) {
        // 遍历从dexPath查询到的dex和资源Element
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            // 如果当前的Element是dex文件元素
            if (dex != null) {
                // 使用DexFile.loadClassBinaryName加载类
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

到此,我们知道class文件是遍历dexElements找到的,而这个dexElements是在DexPathList的构造方法中,通过makeDexElements方法初始化的。

因此,如果我们通过反射,拿到DexPathList的dexElements,然后把含有要替换的.class的dex包装成Element,放到dexElements这个数组的前边,那么通过遍历加载.class的时候就能优先加载我们要替换的.class的文件了。

p.s. 当然,由于ClassLoader对于已经加载的类存在缓存,因此当类被加载过后,不会从新添加的dex文件中进行寻找,只能通过重启生效。


那么最后的问题,如何把.java文件包装为dex文件

很简单,就两步

  1. 通过 javac 命令将.java编译成.class文件
  2. 通过Android的buildTools中的dx.bat将.class编译成.dex,这个bat文件一般路径为 Android\sdk\build-tools\30.0.3\dx.bat

最后放一个demo进行演示

//在Application中进行替换
public class MApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //dex作为插件进行加载
        dexPlugin();
    }
    ...

  /**
     * dex作为插件加载
     */
    private void dexPlugin(){
        //插件包文件
        File file = new File("/sdcard/FixDexTest.dex");
        if (!file.exists()) {
            Log.i("MApplication", "插件包不在");
            return;
        }
        try {
            //获取到 BaseDexClassLoader 的  pathList字段
            // private final DexPathList pathList;
            Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
            //破坏封装,设置为可以调用
            pathListField.setAccessible(true);
            //拿到当前ClassLoader的pathList对象
            Object pathListObj = pathListField.get(getClassLoader());

            //获取当前ClassLoader的pathList对象的字节码文件(DexPathList )
            Class<?> dexPathListClass = pathListObj.getClass();
            //拿到DexPathList 的 dexElements字段
            // private final Element[] dexElements;
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            //破坏封装,设置为可以调用
            dexElementsField.setAccessible(true);

            //使用插件创建 ClassLoader
            DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
            //拿到插件的DexClassLoader 的 pathList对象
            Object newPathListObj = pathListField.get(pathClassLoader);
            //拿到插件的pathList对象的 dexElements变量
            Object newDexElementsObj = dexElementsField.get(newPathListObj);

            //拿到当前的pathList对象的 dexElements变量
            Object dexElementsObj=dexElementsField.get(pathListObj);

            int oldLength = Array.getLength(dexElementsObj);
            int newLength = Array.getLength(newDexElementsObj);
            //创建一个dexElements对象
            Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), oldLength + newLength);
            //先添加新的dex添加到dexElement
            for (int i = 0; i &lt; newLength; i++) {
                Array.set(concatDexElementsObject, i, Array.get(newDexElementsObj, i));
            }
            //再添加之前的dex添加到dexElement
            for (int i = 0; i &lt; oldLength; i++) {
                Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObj, i));
            }
            //将组建出来的对象设置给 当前ClassLoader的pathList对象
            dexElementsField.set(pathListObj, concatDexElementsObject);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

最后对完整的热修复方案进行简单设计

  1. 通过定时任务请求后台,后台通过版本确认是否需要热修复,需要热修复的把对应.dex文件下载到指定文件夹(ex:{path}/hotfix/{app_version})并更新DexPathList的dexElements,如果没有加载过可以直接生效
  2. App每次启动,把指定文件夹的.dex文件更新到DexPathList的dexElements中
  3. 对于紧急的热修复,可以增加弹窗提醒用户重启应用。

注意,用于热修复的.dex文件应该指定版本,不然新的apk中的代码无法被加载。

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值