基于类加载的dex热修复分析

dex文件的热修复方法有很多,例如通过类加载器或者偏底层的实现通过修改ArtMethod。这里只分析基于类加载器的dex热修复原理,实际dex插件化的原理和热修复的原理也有类似之处。

dex热修复原理

android虚拟机中每一个classloader类加载器都有一个对应的DexPathList类,而DexPathList类有一个对应的Elements数组,Elements数组中保存了此classloader类加载器加载的所有dex文件信息,dex文件信息用一个DexFile类来描述。

DexFile中保存了加载的dex文件信息,其中包含了此dex文件包含的所有类名称,在通过主动调用脱函数抽取类壳的时候就是通过枚举apk中所有的类加载器并通过反射最后得到所有加载dex文件对应的DexFile,通过DexFile进一步获取到所有的类名称,通过classloader类加载器调用loadclass主动加载(显式加载)此类,从而使被抽取的函数代码进行回填。

function dealwithClassLoader(classloaderobj) {
    if (Java.available) {
        Java.perform(function () {
            try {
                var dexfileclass = Java.use("dalvik.system.DexFile");
                var BaseDexClassLoaderclass = Java.use("dalvik.system.BaseDexClassLoader");
                var DexPathListclass = Java.use("dalvik.system.DexPathList");
                var Elementclass = Java.use("dalvik.system.DexPathList$Element");
                var basedexclassloaderobj = Java.cast(classloaderobj, BaseDexClassLoaderclass);
                var tmpobj = basedexclassloaderobj.pathList.value;
                var pathlistobj = Java.cast(tmpobj, DexPathListclass);
                var dexElementsobj = pathlistobj.dexElements.value;
                if (dexElementsobj != null) {
                    for (var i in dexElementsobj) {
                        var obj = dexElementsobj[i];
                        var elementobj = Java.cast(obj, Elementclass);
                        tmpobj = elementobj.dexFile.value;
                        var dexfileobj = Java.cast(tmpobj, dexfileclass);
                        const enumeratorClassNames = dexfileobj.entries();
                        while (enumeratorClassNames.hasMoreElements()) {
                            var className = enumeratorClassNames.nextElement().toString();
                            if(-1 != className.indexOf("com.reverccqin")){
                                console.log("start loadclass->", className);
                                var loadclass = classloaderobj.loadClass(className);
                                console.log("after loadclass->", loadclass);
                            }
                        }
                    }
                }
            } catch (e) {
                console.log(e)
            }
        });
    }
}

对于基于类加载器的dex热修复而言,其通过动态下发更新的dex文件,在apk重启后待修改的逻辑类还未加载时通过自定义的类加载器加载此更新的dex文件。然后通过反射将自定义classloader的Element插入到默认classloader的Element之前(简单点直接插入到Element数组头部)。这样在classloader加载更新的目标类时会先从更新的DexFile中加载类,原来的DexFile中的类就不会被加载了(因为相同名称的类在同一个classloader中只能被加载一次),所以这里就要注意一个修改Elment数组的时机,必须在待修改的逻辑类还未被加载到虚拟机中插入自定义的Element。

这样之后显式/隐式加载类的行为都会从热修复后的Element中得到对应的类信息并进行调用执行,热修复实现了不用更新应用程序就可以修改程序执行逻辑的功能,但是缺点是需要重启apk才能生效。如果不重启行不行呢,答案是不行可以看一下类加载的流程,android虚拟机中相同签名的类可以被不具有父子关系(可以是多代)的两个类加载器同时加载(双亲委派),但是对于同一个类加载器或者具有父子关系(可以是多代)的类加载器而言不能加载两个签名相同的类。所以如果待修改的逻辑类已经被默认的classloader加载了,无论是去直接替换Elements还插入Element都无法使修补后的dex文件中的类被再次加载到默认的classloader中。

//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) {
        try {
            if (parent != null) {
                //父加载器不为空就先让父加载器加载此类
                c = parent.loadClass(name, false);
            } else {
                //父加载器为空说明此加载器为根加载器BootClassLoader,让根加载器去加载
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }

        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            //如果父加载器都加载不了就让当前类加载器调用findclass去加载。
            c = findClass(name);
        }
    }
    return c;
}
posted @ 2023-01-03 14:49  怎么可以吃突突  阅读(307)  评论(0编辑  收藏  举报