Android 简单热修复(下)——基于DexClassLoader的实现

前面Java类加载器的介绍中写过关于ClassLoader的基础知识,包括了双亲委派机制、自定义ClassLoader等内容。但是,前面讲到的都是基于JVM的内容,在这里需要清楚下:Android采用的Dalvik虚拟机(DVM)和ART虚拟机(4.4版本发布)。

简单描述Android采用的虚拟机和JVM的区别

送分题(敲黑板)!!



根据广大网友描述,区别如下:

  • Dalvik基于寄存器,而JVM基于栈。基于寄存器的虚拟机对于编译后变大的程序来说,在它们执行的时候,花费的时间更短。
  • JVM运行java字节码,DVM运行的是其专有的文件格式Dex。
  • ART与Dalvik最大的不同在于,在启用ART模式后,系统在安装应用的时候会进行一次预编译,在安装应用程序时会先将代码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。
  • ART占用空间比Dalvik大(字节码变为机器码之后,可能会增加10%-20%),这就是“时间换空间大法”。
  • 预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了 CPU 的使用频率,降低了能耗。

如果非要深究为什么上面的一定是对的?我只能说——我也不懂。。作为菜鸡,只能站在巨人的肩膀看世界了(虽然有的的确不靠谱)。


DVM执行Dex文件

上面讲过:JVM运行java字节码,DVM运行的是其专有的文件格式Dex。Dex文件是由java的.class文件通过Android Sdk的build-tools目录下的dx.bat生成,生成命令如下:

dx --dex --output=[outFilePath] [inputDirPath]

举个例子:

package com;

public class Main {

    public static void main(String[] args) {
        System.out.println("hello");
    }
}

将Main.class文件和其目录拷贝到桌面(主要是为了方便),并执行下面的命令:

执行命令

这里面最后的输入路径需要注意下,输入路径需要是.class包名的上一级目录,否则生成Dex文件会报错。执行命令后会生成文件:
dex文件

接着,我们将dex文件放到/mnt/sdcard/目录下:
放到目录

通过命令adb shell dalvikvm -cp [dexFilePath] [className]执行:
执行结果

OK,DVM执行Dex文件的结果已经出来了。

Android的类加载器

上面已经说了DVM可以执行Dex文件,其实我们也可以知道不管采用什么虚拟机,还是需要将执行的代码(字节码)加载到内存,最终执行。我们先看下Android里的ClassLoader:

image.png

Android的ClassLoader是PathClassLoader,需要源码的可以在这里搜索。
PathClassLoaderBaseDexClassLoader的子类,下面我们来看下源码:

PathClassLoader.java:
public class PathClassLoader extends BaseDexClassLoader {
    // 调用了BaseDexClassLoader的构造方法
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

BaseDexClassLoader.java:
public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        // 创建DexPathList对象
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
        ......
    }

    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }

    // 重写了findClass方法,遵循了双亲委派机制
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        List suppressedExceptions = new ArrayList();
        // 调用pathList的findClass方法
        Class c = pathList.findClass(name, suppressedExceptions);
        // 找到了Class则return
        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;
    }

    ......
}

这两个类的源码并不是很多,主要逻辑还是在BaseDexClassLoader中。BaseDexClassLoader重写了findClass方法,遵循双亲委派机制,并且这里调用了BaseDexClassLoader的成员变量pathListfindClass方法。如果pathList.findClass方法找到了需要的Class,那么将结果返回。我们需要看下DexPathList的源码:

/*package*/ final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String zipSeparator = "!/";

    private final ClassLoader definingContext;
    // 这个属性很重要,热修复的关键
    private Element[] dexElements;
    private final NativeLibraryElement[] nativeLibraryPathElements;

    private final List nativeLibraryDirectories;

    private final List systemNativeLibraryDirectories;

    private IOException[] dexElementsSuppressedExceptions;

    ......

    public DexPathList(ClassLoader definingContext, String dexPath,
                       String librarySearchPath, File optimizedDirectory) {

        ......
        this.definingContext = definingContext;

        ArrayList suppressedExceptions = new ArrayList();
        // save dexPath for BaseDexClassLoader
        // 根据传入的dex的路径生成Element数组
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                suppressedExceptions, definingContext);

        ......
    }

    ......
    private static Element[] makeDexElements(List files, File optimizedDirectory,
                                             List suppressedExceptions, ClassLoader loader) {
        Element[] elements = new Element[files.size()];
        int elementsPos = 0;

        for (File file : files) {
            if (file.isDirectory()) {
                // We support directories for looking up resources. Looking up resources in
                // directories is useful for running libcore tests.
                // 支持目录的形式
                elements[elementsPos++] = new Element(file);
            } else if (file.isFile()) {// 如果是文件的话
                
                String name = file.getName();
                // 文件名以.dex结尾
                if (name.endsWith(DEX_SUFFIX)) {
                    // Raw dex file (not inside a zip/jar).
                    try {
                        // 创建dexFile对象
                        DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
                        // 数组赋值
                        if (dex != null) {
                            elements[elementsPos++] = new Element(dex, null);
                        }
                    } catch (IOException suppressed) {
                        System.logE("Unable to load dex file: " + file, suppressed);
                        suppressedExceptions.add(suppressed);
                    }
                } else {
                    DexFile dex = null;
                    try {
                        dex = loadDexFile(file, optimizedDirectory, loader, elements);
                    } catch (IOException suppressed) {
                        suppressedExceptions.add(suppressed);
                    }
                    
                    // 其他情况,根据loadDexFile返回值确定如何创建
                    if (dex == null) {
                        elements[elementsPos++] = new Element(file);
                    } else {
                        elements[elementsPos++] = new Element(dex, file);
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }
        }
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        }
        return elements;
    }

    private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                       Element[] elements)
            throws IOException {
        // 根据是否传入优化的目录来确定DexFile调用哪种构造方法
        if (optimizedDirectory == null) {
            return new DexFile(file, loader, elements);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
        }
    }

    ......
    // 这里才是重点
    public Class findClass(String name, List suppressed) {
        // 遍历dexElements成员变量,通过Element的findClass方法去查找需要的Class
        // 找到后,直接返回!!
        // 这里是热修复的关键
        for (Element element : dexElements) {
            Class clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
    ......
}

上面代码不少,其实真正有用的我觉得就是findClass方法,DexPathListfindClass方法通过遍历成员变量Element[] dexElements来根据名称查找所需的Class,并将找到的Class返回(如果存在的话),这里非常非常重要!!
写到这里,我想懂的人肯定都懂了,我们需要做的就是将没有问题的代码Dex文件插入到DexPathList的成员变量dexElements前面,这样在读取Class时首先查找的是我们没有问题的Dex文件,当查找成功后直接返回,不会进入后面的循环,从而完成问题代码的“修复”

实现

原理都讲清楚了,剩下的就是实现了。实现代码更加简单,反射修改属性即可。下面请开始我的表演:

public class BugFixUtils {
    private static final String DEX = ".dex";
    // 这个8.1的源码已经无效了
    private static final String OPTIMIZED_DEX_DIR = "newDex";

    public static void doFix(Context context, String newDexPath) {
        File dexFileDir = new File(newDexPath);
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        if (dexFileDir.exists()) {
            File[] dexFiles = dexFileDir.listFiles();
            if (dexFiles != null) {
                for (File dexFile : dexFiles) {
                    if (dexFile.getName().endsWith(DEX)) {
                        // 创建对象
                        File optimizedDirectory = new File(context.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZED_DEX_DIR);
                        if (!optimizedDirectory.exists()) {
                            optimizedDirectory.mkdirs();
                        }
                        try {
                            BaseDexClassLoader baseDexClassLoader = new BaseDexClassLoader(
                                    dexFile.getAbsolutePath(),
                                    optimizedDirectory,
                                    null,
                                    pathClassLoader);
                            // 反射获得属性
                            Object pathListObj = getFieldObj(Class.forName("dalvik.system.BaseDexClassLoader"), baseDexClassLoader, "pathList");
                            Object dexElementsObj = getFieldObj(Class.forName("dalvik.system.DexPathList"), pathListObj, "dexElements");
                            // 获得现在App dex文件属性
                            Object pathListBugObj = getFieldObj(Class.forName("dalvik.system.BaseDexClassLoader"), pathClassLoader, "pathList");
                            Object dexElementsBugObj = getFieldObj(Class.forName("dalvik.system.DexPathList"), pathListBugObj, "dexElements");
                            // 合并,顺序:新的 有Bug的
                            Object newElements = combineArray(dexElementsObj, dexElementsBugObj);
                            // 重新赋值
                            setFieldObj(Class.forName("dalvik.system.DexPathList"), pathListBugObj, newElements, "dexElements");
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    private static void setFieldObj(Class clzz, Object obj, Object value, String field) {
        try {
            Field declaredField = clzz.getDeclaredField(field);
            declaredField.setAccessible(true);
            declaredField.set(obj, value);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private static Object getFieldObj(Class clzz, Object obj, String field) {
        try {
            Field localField = clzz.getDeclaredField(field);
            localField.setAccessible(true);
            return localField.get(obj);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        return null;
    }

    private static Object combineArray(Object newDex, Object bugDex) {
        // 获得数组对象的类型
        Class componentType = newDex.getClass().getComponentType();
        // 获得长度
        int i = Array.getLength(newDex);
        int j = Array.getLength(bugDex);
        // 创建新的数组
        Object result = Array.newInstance(componentType, i + j);
        // 把新的dex文件放在前面,有bug的放在后面
        System.arraycopy(newDex, 0, result, 0, i);
        System.arraycopy(bugDex, 0, result, i, j);
        return result;
    }
}

代码已经完成:

  1. 获取新的dex文件的位置,并根据其后缀(.dex)来判断文件是否为所需。
  2. 遍历这些文件,建立BaseDexClassLoader对象。
  3. 通过反射获得BaseDexClassLoader对象的DexPathList pathList成员变量以及pathList中的Element[] dexElements成员变量。
  4. 通过反射获得PathClassLoader对象的DexPathList pathList成员变量以及pathList中的Element[] dexElements成员变量。
  5. 将两个dexElements数组合并,注意新的dexElements数组要放在有bug的dexElements数组前面。
  6. 将合并后的数组赋值给PathClassLoader对象中的DexPathList pathList成员变量中的Element[] dexElements变量,大功告成!

测试

测试前代码:

测试前

测试前的代码只是在打开Activity的时候显示Toast“测试”,在未加载新的dex文件时正常:
测试前结果

修改后的测试代码,这里将Toast文字改编为“测试之后”,并将.class文件打包成dex文件放到sd卡的根目录下:
测试之后

dex文件

这里需要注意下,需要将App完全杀死后重新打开App,结果如下:
测试之后结果

以上源码是Android 26但是测试机是Android 5.1.1,测试可以成功。用Android 模拟器一直不成功,不知道为什么。。

总结

前面也说过,这篇文章的由来,在看源码的过程中有一种恍然大悟的感觉。之前一直听说简单热修复的原理就是把新的dex插入到旧的dex前面,但是真正让我去说个所以然,感觉真的难。不过看完源码后,原理真的很简单,真的是码读百遍,其义自见!!

你可能感兴趣的:(Android 简单热修复(下)——基于DexClassLoader的实现)