(个人总结向)热修复中的ClassLoader加载机制

个人总结笔记,不具备指导作用,怕说错了说漏了带坏其他同学,有错误的地方希望热心的同学可以指出,感谢

进入正片

首先记录一下在学习类加载机制过程中遇到的两个概念

1.ClassLoader(类加载)

其中包含BootClassLoader(负责加载Android FrameWork层的Class文件)、PathClassLoader(用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex,我们项目自己写的代码就是通过这个ClassLoader加载的,例如MainActivity,可以通过MainActivity.class.getClassLoader()获取到)、DexClassLoader(加载指定的dex,以及jar、zip、apk中的classes.dex)。

PathClassLoader与DexClassLoader之间没有任何区别,只不过DexClassLoader可以自定义odex的保存地址。

2.双亲委托机制

上面说到PathClassLoader负责加载我们自己写的Java文件的class文件,但是当我们查看PathClassLoader的时候发现PatchClassLoader中只有两个构造方法并没有加载class文件的函数,如下:

public class PathClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code PathClassLoader} that operates on a given list of files
     * and directories. This method is equivalent to calling
     * {@link #PathClassLoader(String, String, ClassLoader)} with a
     * {@code null} value for the second argument (see description there).
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    /**
     * Creates a {@code PathClassLoader} that operates on two given
     * lists of files and directories. The entries of the first list
     * should be one of the following:
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
     * well as arbitrary resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ul>
     *
     * The entries of the second list should be directories containing
     * native library files.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @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 PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

于是我们前往PathClassLoader的父类BaseDexClassLoader去寻找加载class的方法,依然没有找到加载class的方法,

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        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的父类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;
            }

            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

        return clazz;
    }

loadClass的参数className就是我们需要查到的class,首先通过findLoadedClass方法在已经加载的class中寻找是否存在className,如果有则返回出去。否则将会调用parent.loadClass(className, false);去parent中调用loadClass方法,parent是什么呢?我们观察ClassLoader的构造方法,其中为parent赋值为ClassLoader中的静态内部类SystemClassLoader.loader。

    /**
     * Constructs a new instance of this class with the system class loader as
     * its parent.
     */
    protected ClassLoader() {
        this(getSystemClassLoader(), false);
    }

    /**
     * Returns the system class loader. This is the parent for new
     * {@code ClassLoader} instances and is typically the class loader used to
     * start the application.
     */
    public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
    }

    static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }

    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        return new PathClassLoader(classPath, BootClassLoader.getInstance());
    }

由此代码我们可以看出最终会创建一个以BootClassLoader为parent的PathClassLoader作为该ClassLoader的parent。

也就是说我们在使用ClassLoader.loadClass()的时候,ClassLoader会一直将类加载的任务交给自己的parent去做,然后一直向上传递,直到传到BootClassLoader。由此我们可以看出双亲委托机制命名的由来——自己不干,让爸爸干,爸爸不干,爷爷干。

使用双亲委托机制的好处是:

1.避免类的重复加载,如果parent加载过了就没有必要自己再加载了,直接拿来用就好了

2.安全,使用双亲委托机制就可以避免我们自定义一个与Java核心api中相同的类名的情况下取代Java核心api导致出现问题,例如我们自己写了一个String类来代替Java中的String类,因为使用了双亲委托机制,Java中的String类已经被加载了,就不会再加载我们自己写的类了。保证了Java核心功能不被替换。

上面讲完了loadClass的过程,然后我们发现如果我们需要的类在在parent中都找不到,这个时候就要靠我们自己了,就需要调用findClass方法。我们在逐级向上查找loadClass方法的过程中发现BaseDexClassLoader有重写ClassLoader的findClass方法,我们来看一下

@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        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;
    }

Class c = pathList.findClass(name, suppressedExceptions);

这行代码就返回了我们需要的class否则就会直接报ClassNotFoundException。

其中pathList是一个DexPathList的对象,我们再前往DexPathList查看DexPathList.findClass

public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

发现DexPathList.findClass又调用了DexFile.loadClassBinaryName方法去获取class。于是我们还要前往DexFile寻找DexFile.loadClassBinaryName

。。。路是真的远。

最终我们在DexFile中发现其调用defineClassNative(name, loader, cookie);得到class,这是一个native方法,其中的细节逻辑我们就无从得知了。搜寻之路到此结束。

回到DexPathList.findClass,我们发现他是通过遍历dexElements数组获取每个子项并获取子项中的dex文件,之后再从dex文件中获取到对应的class。dexElements是如何得到的呢?我们通过阅读代码发现在DexPathList的构造函数中对dexElements进行了初始化:

    // save dexPath for BaseDexClassLoader
    this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
                                            suppressedExceptions);

    /**
     * Makes an array of dex/resource path elements, one per element of
     * the given array.
     */
    private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                              List<IOException> suppressedExceptions) {
        List<Element> elements = new ArrayList<>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            File zip = null;
            File dir = new File("");
            DexFile dex = null;
            String path = file.getPath();
            String name = file.getName();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                zip = new File(split[0]);
                dir = new File(split[1]);
            } else if (file.isDirectory()) {
                // We support directories for looking up resources and native libraries.
                // Looking up resources in directories is useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } 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) {
                        /*
                         * IOException might get thrown "legitimately" by the DexFile constructor if
                         * the zip file turns out to be resource-only (that is, no classes.dex file
                         * in it).
                         * Let dex == null and hang on to the exception to add to the tea-leaves for
                         * when findClass returns null.
                         */
                        suppressedExceptions.add(suppressed);
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements.add(new Element(dir, false, zip, dex));
            }
        }

        return elements.toArray(new Element[elements.size()]);
    }


    

由于我们的class是从dex文件中读取出来的,因此进入

    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);
                    }
                }

从这里获取到我们需要的dex并以Element(dir, false, zip, dex)的形式添加进dexElements。

到这里我们简单总结一下

总结

类在加载的过程中需要调用ClassLoader的loadClass进行加载,首先前往已经加载的内容中寻找,如果没找到则使用双亲委托机制parent.loadClass()进行加载,如果发现“家里人”都帮不上忙的时候,就需要自己来findClass。findClass的过程是通过加载dex文件进入dexElements数组并通过遍历找到对应名字的class,找到便返回出来。

由此我们可以确定热修复的思路,现有A.java,其中出现了待修复的bug,我们修复bug后生成了新的A.java,然后将其生成新的dex文件该dex中只需有我们修复后的A.java即可,并通过反射的手段将我们新生成的dex加入到dexElements的头部,这样当findClass再进行for循环的时候,它先找到我们新的dex文件中的A.java进行加载,这样就完成了新的文件替代旧的文件,实现了bug的热修复。

接下来的计划应该是会再总结一下反射的基本知识,主要目的还是为了自我巩固。一开始不想写博客的原因是怕自己写不出什么内容,感觉会和别人写的千篇一律,搞得网上乌烟瘴气的,这篇写的感觉倒是基本上就是我自己的学习成果,写得也比较顺(啰嗦),以后再写写应该会写得更加简洁易懂些吧,坚持吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值