Android基于DexClassLoader实现热修复

本文简单介绍 热修复的一种实现,插件化原理也可以参考。

Android系统中的类加载器

  • Android系统屏蔽了ClassLoader的findClass加载方法,那么它自己的类加载时通过什么样的方式实现的呢?

  • Android系统中有两个类加载器分别为PathClassLoader和DexclassLoader。

  • PathClassLoader和DexClassLoader都是继承与BaseDexClassLoader,BaseDexClassLoader继承与ClassLoader。

Android为什么会将自己的类加载器派生出两个不同的子类,它们各自有什么用?

PathClassLoader和DexClassLoader比较

PathClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

DexClassLoader

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

BaseDexClassLoader的构造函数

    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

optimizedDirectory:制定输出dex优化后的odex文件,可以为null
libraryPath:动态库路径(将被添加到app动态库搜索路径列表中)
parent:制定父类加载器,以保证双亲委派机制从而实现每个类只加载一次。

可以看出 PathClassLoader和DexClassLoader的区别就在于构造函数中optimizedDirectory、libraryPath这两个参数。PathClassLoader中optimizedDirectory、libraryPath为null,DexClassLoader中为new File(optimizedDirectory)、libraryPath。

optimizedDirectory
BaseDexClassLoader的构造函数利用optimizedDirectory创建了一个DexPathList,看看DexPathList中optimizedDirectory:


public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    /******部分代码省略******/
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
    /******部分代码省略******/
}
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
   /******部分代码省略******/
    for (File file : files) {
        /******部分代码省略******/
        if (file.isDirectory()) {
           /******部分代码省略******/
        } else if (file.isFile()){
            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);
                }
            } else {
                zip = file;
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

由以上可以得出以下结论

DexClassLoader:能够加载自定义的jar/apk/dex
PathClassLoader:只能加载系统中已经安装过的apk
所以Android系统默认的类加载器为PathClassLoader,而DexClassLoader可以像JVM的ClassLoader一样提供动态加载。

Android系统中类加载的双亲委派机制

Android5.1源码中ClassLoader的loadClass方法

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                //先让父类加载器加载
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }
            //当所有父类节点的类加载器都没有找到该类时,当前加载器调用findClass方法加载。
            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

想要动态加载类,可以用 自定义ClassLoader和双亲委派机制 中自定义ClassLoader的方法加载自己定义的class文件么?看看Android源码中的ClassLoader的findClass方法:
protected Class<?> findClass(String className) throws ClassNotFoundException {
throw new ClassNotFoundException(className);
}
这个方法直接抛出了“ClassNotFoundException”异常,所以在Android中想通过这种方式实现类的加载时不行的。

总结

ClassLoader的loadClass方法保证了双亲委派机。
BaseDexClassLoader提供了两种派生类使我们可以加载自定义类。

----------------------基于以上介绍下面进入热修复的内容-------------------------------

修复流程

在这里插入图片描述
在这里插入图片描述
两张图 第一张是修复流程,第二张思路都在图片中写的比较详细了,不再啰嗦,下面程序实现以上功能。

第一步先把修复bug的包 classes2.dex 拷贝到程序的私有目录下

 /**
     * 修复原理
     * 使用DexClassLoader:能够加载未安装的jar/apk/dex
     * DexClassLoader ->DexPathlist->dexElements[]
     * 将修复bug的dex包与dexElement[] 合并成一个新的dex,并把fix.dex 放到 数组的前面
     * 通过反射将 原来的dexElements[]用 合并后的的dexElements[] 替换掉,这样就可不会加载旧的带有bug的class文件。
     * @param view
     */
    public void fixBug(View view) {
        //修复bug
        //1.修复包 classes2.dex
        String dexName = "classes2.dex";
        File sourceFile = new File(Environment.getExternalStorageDirectory(),dexName);

        //DexClassLoader指定的应用程序目录
        File targetFile = new File(getDir("odex", Context.MODE_PRIVATE).getAbsolutePath()+File.separator+dexName);
        if(targetFile.exists()){
            targetFile.delete();
        }
        //拷贝到私有路径
        FileUtil.copyFile(sourceFile,targetFile);

        FixDexUtil.loadFixedDex(this);

    }

第二部就是进行合并dex数组,将我们的修复后的包classes2.dex,与当前app的 dexElement 进行合并,生成一个新的DexElement。

以下主要基于反射来实现替换出bug的class 。

获取 DexPathList 中的 DexElement ,然后将合并后的 NewDexElement 替换掉原来的。这样就达到了修复的目的,这也是插件化的一种实现原理。

     //dex文件需要被写入的目录
        String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
        File fileOpt = new File(optimizeDir);
        if(!fileOpt.exists()){
            fileOpt.mkdirs();
        }
        //1.获得加载应用程序dex的PathClassLoader
        PathClassLoader systemClassLoader = (PathClassLoader) context.getClassLoader();
        for (File dexFile : loadedDex) {
            //DexClassLoader构造参数
            //String dexPath,dex路径
            // String optimizedDirectory, 制定输出dex优化后的odex文件,可以为null
            // String librarySearchPath, so路径 动态库路径(将被添加到app动态库搜索路径列表中)
            // ClassLoader parent 制定父类加载器,以保证双亲委派机制从而实现每个类只加载一次。

            DexClassLoader dexClassLoader = new DexClassLoader(
                    dexFile.getAbsolutePath(),
                    optimizeDir,
                    null,
                    systemClassLoader
                    );

            //3.合并dex
            try {
                Object dexObj = ReflectUtil.getPathList(dexClassLoader);
                Object pathObj = ReflectUtil.getPathList(systemClassLoader);
                Object fixDexElements = ReflectUtil.getDexElements(dexObj);
                Object pathDexElements = ReflectUtil.getDexElements(pathObj);
                //合并两个数组
                Object newDexElements = ReflectUtil.combineArray(fixDexElements,pathDexElements);
                LogUtil.log("combineArray>>>");
                //重新赋值给PathClassLoader 中的exElements数组

                Object pathList = ReflectUtil.getPathList(systemClassLoader);

                ReflectUtil.setField(pathList,pathList.getClass(),"dexElements",newDexElements);
                LogUtil.log("修复完成>>>");
                LogUtil.log("修复完成>>>"+pathList+"   ");
            } catch (Exception e) {
                e.printStackTrace();
                LogUtil.log("doDexInject Exception>>>"+e.toString());
            }

        }

完整的代码就不贴了,想看的点这儿

可供参考
Android热修复(二):以DexClassLoader类加载原理编写demo实现类替换修复
Android类加载之PathClassLoader和DexClassLoader
https://www.jianshu.com/p/197636e75c06
关于安卓简单补丁的制作和热修复的代码实现

欢迎爱学习的小伙伴加群一起进步:230274309 。 一起分享,一起进步!少划水,多晒干货!!欢迎大家!!!(进群潜水者勿加)
发布了134 篇原创文章 · 获赞 197 · 访问量 41万+

猜你喜欢

转载自blog.csdn.net/u011733020/article/details/101454109