绝对干货, 热修复原理,实现方式总结!!!

今天总结一下热修复的实现方式有二种方案:
 1、修改native层代码实现
 2、通过函数插桩实现、双亲委托模式实现

第一种方案(修改native层代码,替换结构体):

AndFix实现原理源码解析:

1、mPatchManager.init(“版本号”)
主要是对patch文件的增加和删除, 内部会对上一次版本号进行判断,如果相同则继续添加,如果不同则删除所有的patch文件

对于patch文件的理解: 是阿里自己定义的协议,方便自己使用, 只不过是把file文件转成patch文件,这样他们就可以根据文件名得到对应的patch文件,就好比我们系统class.dex文件,在源码中他们也会吧dex文件转化成dexfile文件。就是方便使用。

2、mPatchManager.addPatch(“patch路径”):
先把添加apatch文件 copy到patch’s文件夹中 ,然后调用loadPatch(patch) : 指定修复的patch包含的所有的class文件,
刚开始的时候会调用一个空参的mPatchManeger.loadPatch() 方法。 里面也是循环 mPatchs 这个保存所有patch的集合, 然后在循环里面再次调用一下loadPatch(patch) 这个方法
在这里插入图片描述
3、调用loadPatch方法, 得到每个类的class-name(类名称)
在这里插入图片描述

4、调用fix()方法,要先验证签名(必须验证签名,不然安全性问题就不用多说了,随便都可以改了),如果验证通过,会把patch中的file转换成DexFlie,然后循环遍历dexfile文件,通过calss - name 找到对应这个类的class字节码
在这里插入图片描述
PS: calss - name哪里来的,是在loadPatch时候,找出所有patchName集合,遍历,获取的class - name
在这里插入图片描述
5、循环DexFile 得到每个修改类的class字节码然后调用, fixClass()
首先通过反射得到修改这个类的所有方法,然后通过注解 MethodReplace找到修改的方法,然后调用replaceMethod进行替换

在这里插入图片描述

6、从字节码中找到了要替换的方法,最终调用的是react-native方法,是通过C层对dex操作完成最终方法的替换
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

OK我们在来看看native层做了什么操作:
这里还会做一件事就是通过上面得到的修复新的方法信息以及需要修复的旧方法名称来操作,不过这里得先获取到旧方法类型,可以看到修复的新旧方法的签名必须一致,所谓签名就是方法的名称,参数个数,参数类型都必须一致,不然这里就报错的。进而也修复不了了。最后在调用了AndFix的addReplaceMethod方法进行native层的修复工作:
在这里插入图片描述
这里会做虚拟机的区分处理,但是他们大致的处理逻辑都是一致的,这里来看一下dalvik的处理机制:
在这里插入图片描述

总结:

1、把文件都转换成我们的patch类。
2、调用loadPatch方法 通过名字转换成class-name,
3、调用mAndFixManager.fix()把patch文件转成DexFile文件,然后遍历DexFile,根据class-name(类名)找到对应的class文件
4、调用fixClass() 通过反射找到这个类所有的方法,在根据注解MethodReplace找到修改的方法,用过C层对dex修改,替换修改方法

5、只能修改原有的方法,修复的条件有限制,即时生效


第二种方案(类加载机制,实现方案):

类加载器详解:双亲委托? 几个类加载器? 父类关系?点击下面连接
https://blog.csdn.net/weixin_39079048/article/details/88126628

1、在Android中,要加载dex文件中的class文件就需要用到 PathClassLoader 或 DexClassLoader 这两个Android专用的类加载器。
2、PathClassLoader与DexClassLoader的区别:
-PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
-DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。

  PathClassLoader与DexClassLoader都继承于BaseDexClassLoader。
  PathClassLoader与DexClassLoader在构造函数中都调用了父类的构造函数,但DexClassLoader多传了一个optimizedDirectory。

在这里插入图片描述
3、BaseDexClassLoader:
通过观察PathClassLoader与DexClassLoader的源码我们就可以确定,真正有意义的处理逻辑肯定在BaseDexClassLoader中,所以下面着重分析BaseDexClassLoader源码。
1)获取class(加载每个class文件的代码)
类加载器肯定会提供有一个方法来供外界找到它所加载到的class,该方法就是findClass(),不过在PathClassLoader和DexClassLoader源码中都没有重写父类的findClass()方法,但它们的父类BaseDexClassLoader就有重写findClass(),所以来看看BaseDexClassLoader的findClass()方法都做了哪些操作,代码如下:

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 + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;

}

可以看到,BaseDexClassLoader的findClass()方法实际上是通过DexPathList对象(pathList)的findClass()方法来获取class的,而这个DexPathList对象恰好在之前的BaseDexClassLoader构造函数中就已经被创建好了。所以,下面就来看看DexPathList类中都做了什么。

在这里插入图片描述

4、分析DexPathList(classLoader核心属性):
1)构造函数

  private final Element[] dexElements;
    
  public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        ...
        this.definingContext = definingContext;
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
        ...
    }

那接下来无疑是分析makeDexElements()方法了,因为这部分代码比较长,我就贴出关键代码,并以注释的方式进行分析:

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
    // 1.创建Element集合
    ArrayList<Element> elements = new ArrayList<Element>();
    // 2.遍历所有dex文件(也可能是jar、apk或zip文件)
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();
        ...
        // 如果是dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            dex = loadDexFile(file, optimizedDirectory);

        // 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)
        } else {
            zip = file;
            dex = loadDexFile(file, optimizedDirectory);
        }
        ...
        // 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    // 4.将Element集合转成Element数组返回
    return elements.toArray(new Element[elements.size()]);
}

在这个方法中,看到了一些眉目,总体来说,DexPathList的构造函数是将一个个的程序文件(可能是dex、apk、jar、zip)封装成一个个Element对象,最后添加到Element集合中。

2)findClass()
再来看DexPathList的findClass()方法:

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        // 遍历出一个dex文件
        DexFile dex = element.dexFile;

        if (dex != null) {
            // 在dex文件中查找类名与name相同的类
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

结合DexPathList的构造函数,其实DexPathList的findClass()方法很简单,就只是对Element数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。
为什么是调用DexFile的loadClassBinaryName()方法来加载class?这是因为一个Element对象对应一个dex文件,而一个dex文件则包含 多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件!!!这可以从Element这个类的源码和dex文件的内部结构看出。。

在这里插入图片描述

实现原理:第一次启动无效

经过分析可以知道,经过对PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,我们知道,安卓的类加载器在加载一个类时会先从自身DexPathList对象中的Element数组中获取(Element[] dexElements)到对应的类,之后再加载。采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex文件。
 在for循环中,首先遍历出来的是dex文件,然后再是从dex文件中获取class,所以,我们只要让修复好的class打包成一个dex文件,放于Element数组的第一个元素,这样就能保证获取到的class是最新修复好的class了(当然,有bug的class也是存在的,不过是放在了Element数组的最后一个元素中,所以没有机会被拿到而已,是因为双亲委托模式)。

在这里插入图片描述

总结: 现在市场上实现的方面基本都是上面两种方案原理。

发布了51 篇原创文章 · 获赞 78 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_39079048/article/details/88971962