tinker热修复核心代码解析

作者: javalong | 来源:发表于2018-08-09 01:23 被阅读20次

    实现原理

    重置dex加载顺序,把修复后的class所在的dex提前加载,由于包名类名一致,所以后面的有问题的class就不会被加载。

    代码实现

    private fun fix() {
            //1.把内存卡中的.dex移动至app私有目录,防止被恶意修改
            val dexPath = "${Environment.getExternalStorageDirectory()}${File.separator}out.dex"
            val newDexPath = copyFile(File(dexPath),getDir("fix_dexs",Context.MODE_PRIVATE))
    
            //2.DexClassLoader加载.dex,然后反射获取Element[]
            val optimizedDir = getDir("dex_opt",Context.MODE_PRIVATE)
            if(!optimizedDir.exists()){
                optimizedDir.mkdir()
            }
            val dexClassLoader = DexClassLoader(newDexPath,optimizedDir.absolutePath,null,classLoader)
            val field = BaseDexClassLoader::class.java.getDeclaredField("pathList")
            field.isAccessible = true
            val pathList = field.get(dexClassLoader)
            val dexPathListCls = Class.forName("dalvik.system.DexPathList")
            val dexElementsFiled = dexPathListCls.getDeclaredField("dexElements")
            dexElementsFiled.isAccessible = true
            val dexElements = dexElementsFiled.get(pathList)
            
            //3.PathClassLoader反射获取Element[]
            val pathList2 = field.get(classLoader)
            val dexElements2 = dexElementsFiled.get(pathList2)
    
    
            //4.合成新的Element[]替换调PathClassLoader中的Element[]
            val length1 = (dexElements as kotlin.Array<*>).size
            val length2 = (dexElements2 as kotlin.Array<*>).size
            val newElements = Array.newInstance(dexElements::class.java.componentType,length1+length2) as kotlin.Array<Any>
            for(i in 0 until length1){
                newElements[i] = dexElements[i]!!
            }
            for(i in 0 until length2){
                newElements[i+length1] = dexElements2[i]!!
            }
            dexElementsFiled.set(pathList2,newElements)
        }
    
        //拷贝文件
        private fun copyFile(file: File, dir: File):String {
            if(!dir.exists()){
                dir.mkdir()
            }
            val dexName = file.name
            val newPath = File(dir,dexName)
            if(!newPath.exists()){
                newPath.createNewFile()
            }
            val fis = FileInputStream(file)
            val fos = FileOutputStream(newPath)
            var temp=0
            while (true){
                temp = fis.read()
                if(temp==-1)break
                fos.write(temp)
            }
            fis.close()
            fos.close()
            return newPath.absolutePath
        }
    

    代码解析

    其实代码上有注释,这里我归纳下。

    1.把内存卡中的.dex移动至app私有目录,防止被恶意修改
    这一步呢比较简单,就是io操作一下就好了,目的就是把修复后的dex放在自己的私有的文件夹下比较安全。

    在讲第2,3点之前,我们首先通过源码了解下加载的机制。

    libcore/ojluni/src/main/java/java/lang/ClassLoader.java

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
                // First, check if the class has already been loaded
                //如果前面加载过了就直接返回
                Class<?> c = findLoadedClass(name);
                ...
                    if (c == null) {
                        //****重点****
                        c = findClass(name);
                    }
                }
                return c;
        }
    
    //直接查看子类BaseDexClassLoader,这里是抛出异常
     protected Class<?> findClass(String name) throws ClassNotFoundException {
            throw new ClassNotFoundException(name);
        }
    

    /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

    ...
     private final DexPathList pathList;
    ...
    @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
           ...
            //很显然是利用 pathList去加载类
            Class c = pathList.findClass(name, suppressedExceptions);
            if (c == null) {
            ...
            }
            return c;
        }
    

    /libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

    public Class<?> findClass(String name, List<Throwable> suppressed) {
            for (Element element : dexElements) {
                Class<?> clazz = element.findClass(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
    ...
        }
    ...
    static class Element{
     ...
    public Class<?> findClass(String name, ClassLoader definingContext,
                    List<Throwable> suppressed) {
                return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
               
    ...
    }
    

    到这里呢,我们可以看出,其实一个个Element就对应了一个个DexFile,也就是对应了我们一个个.dex文件。

    所以大家这时候再看下前面所说的实现原理,只要在这个for循环遍历Element的时候先遍历我们最新修复的Dex文件,然后找到了我们最新修复的class文件的话,就能实现热修复了。所以下面采用反射替换掉dexElements这个变量。
    在看下面第2,3点前,我们还需要了解下,DexClassLoader,PathClassLoader,'BaseDexClassLoader'的关系和区别。

    由于我们这里不是主要介绍ClassLoader,所以大家具体可以参考下
    https://www.jianshu.com/p/4b4f1fa6633c
    这篇文章。

    总之,DexClassLoader用来加载app外部的dex文件,比如我们热更新修复的class打成的dex文件。PathClassLoader用来加载apk文件内部的dex文件,也就是用来加载app内部本来就有的class。而BaseDexClassLoader 是它们的父类。

    2.DexClassLoader加载.dex,然后反射获取Element[]
    3.PathClassLoader反射获取Element[]

    2,3点分别是反射获取到热修复后的dex文件和app内部原有的dex文件所对应的Element数组。这里值得一提的是dalvik.system.DexPathList,这个类是不能直接使用的,需要通过Class.forName去反射调用。

    4.合成新的Element[]替换调PathClassLoader中的Element[]
    这里把2个Element数组合成了一个新的Element数组,值得注意的是,需要把热修复的对应的Element数组放在前面。还有一点就是dexElementsFiled.set(pathList2,newElements)
    pathList2指的是PathClassLoader中的DexPathList,这里必须要替换调用PathClassLoader中的DexPathList,因为app内部加载class使用的是PathClassLoader,而不是 DexClassLoader

    个人所得

    前面一直理解为,tinker只能重新启动才能生效,这是不对的,我们可以直接从原理上去看,只要你的有问题的class没有去加载,这时候去下载修复后的dex文件然后加载修复,就不必一定要去重新启动app才生效了。

    总结

    原理很简单,大家可以自己写着练习下,需要了解andfix实现原理的同学可以参考:https://www.jianshu.com/p/3a1c4c89225a
    实现的原理和机制都是不一样的,可以了解一下。

    相关文章

      网友评论

      • wsxiaoluob:牛逼,深刻,好用,贼特么棒棒极了!

      本文标题:tinker热修复核心代码解析

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