/**
*每天一个知识点day65 TODO 热修复原理
*
* https://www.jianshu.com/p/e179fcc97666
* https://www.jianshu.com/p/0399f5e4cdb2
* https://www.jianshu.com/p/cb1f0702d59f
*
* 热修复能完成代码修复、资源修复、so库修复
*
* 热修复技术 百花齐放
* Dexposed 阿里 实时修复 Native hook
* Andfix 阿里 实时修复 Native hook
* 阿里百川Hotfix 阿里 实时修复 Native hook 混合
* Sophix 阿里 实时修复+冷启动修复
* Qzone超级补丁 腾讯 冷启动修复 Java
* QFix 腾讯 冷启动修复 Java
* Robust 美团 实时修复 Java
* Nuwa 大众点评 冷启动修复 Java
* RocooFix 百度 冷启动修复 Java
* Aceso 美丽说蘑菇街 实施修复
* Amigo 饿了么 冷启动修复 Java
* Tinker 微信 冷启动修复 Java
*
*
*
*
* 代码热修复原理主要是类替换,Android中可以动态加载代码的ClassLoader有
* PathClassLoader和DexClassLoader。
*
* 因为PathClassLoader只能加载已经安装到Android系统中的apk文件(data/data目录),
* 是安卓默认使用的类加载器
* 而DexClassLoader在Dalvik和ART虚拟机中可以加载任意目录下的dex/jar/apk/zip文件,
* 但是需要指定一个optimizedDirectory,所以热修复使用DexClassLoader来加载补丁包中的类。
*
* DexClassLoader extends BaseDexClassLoader extends ClassLoader。
*
* PathClassLoader与DexClassLoader都继承于BaseDexClassLoader。
* PathClassLoader与DexClassLoader在构造函数中都调用了父类的构造函数,
* 但DexClassLoader多传了一个optimizedDirectory。
*
* public class BaseDexClassLoader extends ClassLoader {
* ...
* public BaseDexClassLoader(String dexPath,
* File optimizedDirectory,
* String libraryPath,
* ClassLoader parent){
* super(parent);
* this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
* }
* ...
* }
* dexPath:要加载的程序文件所在目录,一般是dex文件,也可以是jar/apk/zip文件。
* optimizedDirectory:dex文件的输出目录
* (因为在加载jar/apk/zip等压缩格式的程序文件时会解压出其中的dex文件,
* 该目录就是专门用于存放这些被解压出来的dex文件的)
* libraryPath:加载程序文件时需要用到的库路径。
* parent:父加载器。
*
* Android 8.0开始,optimizedDirectory过时,不再生效。
*
* 从一个完整App的角度来说,程序文件指定的就是apk包中的classes.dex文件;
* 但从热修复的角度来看,程序文件指的是补丁。
*
* 类加载器肯定会提供有一个方法来供外界找到它所加载到的class,该方法就是findClass()
* BaseDexClassLoader的findClass():
* public class BaseDexClassLoader extends ClassLoader {
* // 需要加载的dex列表
* private final DexPathList pathList;
* public BaseDexClassLoader(String dexPath,
* File optimizedDirectory,
* String libraryPath,
* ClassLoader parent) {
* super(parent);
* this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
* }
*
* @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;
* }
* }
* DexPathList在BaseDexClassLoader构造函数中被创建,BaseDexClassLoader的findClass方法是核心。
*
* Element类
* static class Element {
* private final File file;
* private final boolean isDirectory;
* private final File zip;
* private final DexFile dexFile;
*
* private ZipFile zipFile;
* private boolean initialized;
*
* // file文件,是否是目录,zip文件通常都是apk或jar文件,dexFile就是.dex文件
* public Element(File file, boolean isDirectory, File zip, DexFile dexFile) {
* this.file = file;
* this.isDirectory = isDirectory;
* this.zip = zip;
* this.dexFile = dexFile;
* }
* }
*
* DexPathList的构造函数做了什么事?
* DexPathList的findClass()方法是怎么获取class的?
*
* private final Element[] dexElements;
* public DexPathList(ClassLoader definingContext, String dexPath,
* String libraryPath, File optimizedDirectory) {
* ...
* this.definingContext = definingContext;
* //splitDexPath(dexPath)方法将dexPath目录下的所有程序文件转变成一个File集合。
* dexPath是一个用冒号(":")作为分隔符把多个程序文件目录拼接起来的字符串(如:/data/dexdir1:/data/dexdir2:...)。
* this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
* ...
* }
*
* 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集合中。
*
* 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;
* }
*
* findClass方法是从Element数组中拿出一个个dex文件,再从dex文件中搜索class,
* 正因为这个特性我们只需要把补丁文件作为Element数组的首个元素,就可以实现动态修复bug了。
*
* 为什么是调用DexFile的loadClassBinaryName()方法来加载class?
* 这是因为一个Element对象对应一个dex文件,而一个dex文件则包含多个class。
* 也就是说Element数组中存放的是一个个的dex文件,而不是class文件
* 这可以从Element这个类的源码和dex文件的内部结构看出。
*
* 原理:
* apk在安装以后,会复制apk到data/app/packageName~1/base.apk
* apk解压会有patch.dex classes.dex classes1.dex classes2.dex classes3.dex...
* 他们都会在dexElement[]数组里面。
*
* 安卓的类加载器在加载一个类时会先从自身DexPathList对象中的Element数组
* 中获取(Element[] dexElements)到对应的类,之后再加载。
* 采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex文件。
* 在for循环中,首先遍历出来的是dex文件,然后再是从dex文件中获取class,
* 所以,我们只要让修复好的class打包成一个dex文件,放于Element数组的第一个元素,
* 这样就能保证获取到的class是最新修复好的class了(当然,有bug的class也是存在的,
* 不过是放在了Element数组的最后一个元素中,所以没有机会被拿到而已)。
*/
image.png
image.png
网友评论