tinker热修复核心代码解析

实现原理

重置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
实现的原理和机制都是不一样的,可以了解一下。

    原文作者:javalong
    原文地址: https://www.jianshu.com/p/861c42beeb50
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞