Android(安卓)热修复Tinker原理分析

作者: 小红军storm | 来源:发表于2018-09-04 19:34 被阅读273次

    1、先说dex文件的加载和类的查找过程
    1.1、dex文件的加载过程
    Java层
    通过我们会通过创建一个DexClassLoader来加载我们的dex,下面就以此为切入点进行

    dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());
    
    //查看DexClassLoader的构造方法。
    public class DexClassLoader extends BaseDexClassLoader {
        // dexPath:是加载apk/dex/jar的路径
        // optimizedDirectory:是优化dex后得到的.odex文件的输出路径
        // libraryPath:是加载的时候需要用到的so库
        // parent:给DexClassLoader指定父加载器
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), libraryPath, parent);
        }
    }
    
    //可以看到它调用的是父类的构造函数,所以直接来看BaseDexClassLoader的构造函数。
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    
    //创建了一个DexPathList实例,下面来看看DexPathList的构造函数。
    private final Element[] dexElements;
    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);
    }
    
    //它调用的是makeDexElements方法来创建一个Element数组来存放Element对象,每个Element对象包含一个DexFile对象。
    private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
        ArrayList<Element> elements = new ArrayList<Element>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            File zip = null;
            DexFile dex = null;
            String name = file.getName();
    
            // 如果是一个dex文件
            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            // 如果是一个apk或者jar或者zip文件
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                zip = file;
    
                try {
                    // 1、调用loadDexFile加载dex文件,得到一个DexFile对象
                        loadDexFile通过c++层native方法去加载dex文件
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                   
                    suppressedExceptions.add(suppressed);
                }
            } else if (file.isDirectory()) {
                elements.add(new Element(file, true, null, null));
            } else {
                System.logW("Unknown file type for: " + file);
            }
            
            // 2、把DexFile对象封装到Element对象中,然后将Element对象加入Element数组
            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, false, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }
    

    dex文件的加载流程:我们会使用DexClassLoader去加载dex文件,DexClassLoader会将这个任务委派给DexPathList中的makeDexElements方法,在makeDexElements中调用了native层的 c++方法去真正的加载dex文件,然后返回DexFile的对象,通过这个对象构建一个Element的对象,然后将这个Element添加到dexElements的数组中。

    1.2、类的查找过程

    //DexClassLoader间接调用父类findClass方法,findClass方法中调用DexPathList中的DexPathList方法
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(parent);
            this.originalPath = dexPath;
            this.pathList =
                new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
        }
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            Class clazz = pathList.findClass(name);
            if (clazz == null) {
                throw new ClassNotFoundException(name);
            }
            return clazz;
        }
    
    //看DexPathList中的findClass方法,可以看到它是遍历dexElements数组,到每个dex文件去寻找当前需要的类,找到之后直接返回不往下找了
        public Class findClass(String name) {
            for (Element element : dexElements) {
                DexFile dex = element.dexFile;
                if (dex != null) {
                    Class clazz = dex.loadClassBinaryName(name, definingContext);
                    if (clazz != null) {
                        return clazz;
                    }
                }
            }
            return null;
        }
    

    类的查找过程:DexClassLoader通过findClass去查找一个类,同样它也是委派给DexPathList的findClass去查找,在DexPathList的findClass中会去遍历我们上面创建的dexElements数组,然后在每个dex中去查找相应的类,找到之后就返回,不再向后查找。

    2、Tinker原理分析

    2.1、先看tinker-补丁包合成流程图 tinker-补丁包合成流程图.png

    补丁包的合成流程:当tinker收到补丁包bug path后,它会开启一个service,和当前有问题的bug dex和成为一个新的fixed dex文件,然后置于tinker dex文件加载路径。

    2.2、再看tinker-合成后的补丁包加载流程 tinker-合成后的补丁包加载流程.png

    补丁包的合成流程:获取到fixed dex文件后,通过反射DexPathList中的makeDexElements方法,将fixed dex插入到Elements数组的最前面。classloader在寻找bug class的时候,找到的就是最前面的dex文件中我们已修复的fixed class。

    3、为什么使用tinker

    在调研热修复的过程中,我们主要调研了Andfix、QQ控件热修复、Tinker。

    3.1、Andfix是在class加载到dalvik之后,采用在native层进行方法替换,达到修复bug method的目的。这样导致了两个问题:1、由于class加载之后,其field数值无法改变(要是能改变,通过这个class创建的对象就失效了),故其不能增加或者减少成员变量;2、因为是动态的,跳过了类的初始化,所以对于静态方法、静态成员变量、构造方法处理可能会有问题,另外增加类也是不可能的;3、由于其采用在native层去进行方法替换,不同厂商手机可能对native层代码做修改,故其兼容性可能较差。基于以上3个缺点,由于我们的app对兼容性要求较高,且可能对方法之外的地方做修改,所以Andfix是不满足我们的需求的。


    Andfix bug method 修复流程.png

    3、2、QQ空间的热修复方案和Tinker方案较为类似,都是通过操作dexElements数组替换有问题的class来实现的,不同的是Tinker时将差分包和有问题的dex文件合成一个新的 fix dex插入到dexElements数组前面的,而QQ空间的热修复方案是直接将差分包,插入到dexElements的前面。这样会导致引用类和被引用类不在同一个dex的错误:dex文件在dalvik执行之前会转化成odex文件,在这个过程中会对dex中每个类做一个检验:如果引用类(A)的直接引用的类(B)和这个类(A)在同一个dex,这个引用类(A)会被打上CLASS_ISPREVERIFIED标记,在引用类(A)调用被引用类的时候,就会去判断这个类(A)和被引用类是否在一个dex,不在,则报错。QQ空间的热修复方案是直接将差分包,插入到dexElements的前面,如果这个被引用类(B)是我们修复的方法,它就会和其被引用类(A)处于不同的dex文件,这样在引用类(A)调用被引用类(B)的时候,就会报错。QQ空间的热修复方案的解决方法是在编译的时候使用aop给每个类的构造函数中插入一段代码,这段代码引用了一个特殊的类(C),在加载的时候将这个特殊的类单独打进一个dex文件,这样所有的类在dex转化成odex的过程中都不会被打上标记,这样就避免了上述错误的发生。但是QQ空间的热修复方案有一个致命的问题就是aop插桩的性能问题。不管是否下发path,都会拖慢app的启动速度,我们的app对性能有严格的要求,故淘汰了QQ空间的热修复方案。

    3.3、tinker原理前面介绍过了,它的问题在于1、由于Tinker采用的将fix dex 插入到dexElements最前面的方式去修复bug,所有它需要重启,不能即时生效。2、Tinker在下发path的时候需要启动一个service去将path和有问题的dex文件合成一个新的dex文件,这个过程是对性能影响较大的,但是合成只需要一次,后续就不需要了,这样的性能损耗还是可以接受的;另外Tinker的覆盖的功能比较全面,对类、成员变量、方法、资源文件、so文件都支持,所有我们最后选择了Tinker。

    4、参考文章
    4.1、Andfix原理:https://blog.csdn.net/qxs965266509/article/details/49816007
    4.2、Andfix原理:http://w4lle.com/2016/03/03/Android%E7%83%AD%E8%A1%A5%E4%B8%81%E4%B9%8BAndFix%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/index.html
    4.3、QQ空间的热修复方案:https://cloud.tencent.com/developer/article/1004417
    4.4、Tinker:http://w4lle.com/2016/12/16/tinker/
    4.5、Tinker:https://www.jianshu.com/p/8edd8cd83423

    相关文章

      网友评论

      本文标题:Android(安卓)热修复Tinker原理分析

      本文链接:https://www.haomeiwen.com/subject/tednwftx.html