美文网首页热修复
Android 类加载机制以及基于类加载机制的热修复

Android 类加载机制以及基于类加载机制的热修复

作者: 0青衣小褂0 | 来源:发表于2019-03-01 19:38 被阅读107次

    android 与 java 的类加载器

    类别 加载文件 类加载器分类
    java .class 文件 {{java类加载机制}}
    android .dex 文件 {{android类加载机制}}

    java 类加载机制

    1. BootStrapClassLoader:
      启动类加载器
      由 C/C++代码实现
      加载 .../jre/lib 下类库 / -Xbootclasspath 参数指定的路径中的类库
      顶级类加载器

    2. ExtClassLoader
      扩展类加载器
      sun.misc.Launcher 中的内部类(即 sun.misc.Launcher$ExtClassLoader)
      加载 .../jre/lib/ext 下的类库 / -Djava.ext.dirs 参数指定的路径中的类库
      parent: 无,此时可以看做 parent 就是 BootStrapClassLoader

    3. AppClassLoader
      应用程序类加载器
      sun.misc.Launcher 中的内部类 (即 sun.misc.Launcher$AppClassLoader)
      加载 classpath 所指定的类库
      parent: ExtClassLoader

    4. 自定义 classLoader
      继承 ClassLoader
      加载自行指定位置的类库
      parent: AppClassLoader

    • java 类加载机制采用双亲委派机制:
    protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //1.检查请求加载类是否已经加载
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //2.请求加载类还未加载,parent!=null,通过parent加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //3.parent == null,尝试采用BootStrapClassLoader启动类加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
    
                //4.以上三步均未成功加载,通过本身加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
    
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    
    • 自定义 classLoader
    //继承ClassLoader
    public class MyClassLoader extends ClassLoader {
        private String classPath;
    
        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }
    
        //重写findClass(String name)方法
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = getByte(name);
                //调用defineClass(name, data, 0, data.length)返回class
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
    
        //获取流
        private byte[] getByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(name);
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }
    }
    

    Android 类加载器机制

    注: 除了 classLoader 与 BootClassLoader,可以通过 as 直接查看,其他均无法直接查看,源码取自互联网

    1. BootClassLoader
      java.lang.ClassLoader 中的内部类(即 java.lang.ClassLoader$BootClassLoader)
      预加载常用类

    2. PathClassLoader
      dalvik.system.PathClassLoader
      加载已经安装的 Apk(/data/app/package)的 Apk 文件
      只有构造方法

    package dalvik.system;
    
    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);
        }
    }
    
    1. DexClassLoader
      dalvik.system.DexClassLoader
      加载任意位置的 dex/zip/apk/jar
      只有构造方法
    package dalvik.system;
    import java.io.File;
    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), libraryPath, parent);
        }
    }
    
    1. BaseDexClassLoader
      PathClassLoader/DexClassLoader 均继承于 BaseDexClassLoader
    • PathClassLoader 与 DexClassLoader 的区别
      只有构造函数所传参数不同
      DexClassLoader 比 PathClassLoader 多传 optimizedDirectory: dex 优化缓存路径
      optimizedDirectory : 用于存放:加载 jar/apk/zip 等压缩格式的程序文件时解压出其中的 dex 文件,如果本身只是 dex 文件,也会进行复制到此文件中
      必须是需要加载的程序目录:即 data/data/packname/xxx
    • android 中的 java.lang.ClassLoader 与 java 中的 java.lang.ClassLoader 并不相同 *
    //可以看到android是无法通过defineClass来获取class类的
    protected final Class<?> defineClass(String name, byte[] b, int off, int len)throws ClassFormatError{
            throw new UnsupportedOperationException("can't load this type of class file");
    }
    
    • DexClassLoader 类加载流程
    • BaseDexClassLoader
    private final DexPathList pathList;
    
    /**
     * 首先执行ClassLoader构造方法
     * 之后执行PathClassLoader/DexClassLoader的构造方法
     * 创建DexPathList对象
     */
    public class BaseDexClassLoader extends ClassLoader {
        ...
        public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
            super(parent);
            this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
        }
        ...
    }
    
    //findclass是交由构造方法中所创建的DexPathList对象来执行的
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        // 实质是通过pathList的对象findClass()方法来获取class
        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;
    }
    
    • DexPathList
    private final Element[] dexElements;
    
    /**
     * 创建Element[]数组
     * dexPath是要加载的dex/zip/jar/apk的原始路径,支持多个以:分割
     * 遍历存入优化缓存路径当中
     * 包装成Element对象,以Element[]数组的形式返回
     */
    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        ...
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
        ...
    }
    
    /**
     * 遍历Element[]数组
     * 找到name所对应的DexFile
     * 由DexFile加载dex文件,值得注意的是,加载成功一个就会直接返回,后面有相同name的也不会加载了
     * 通过类加载机制实现的热修复就是采用这个机制,将修复类的dex放在Element[]数组的最前方,则只会加载修复类的dex
     * 而不会去加载本身所具有的dex即具有bug的dex
     */
    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;
    }
    

    基于 android 类加载机制实现的热修复

    从以上的分析可以得知:

    • DexClassLoader 创建
      构造方法传入希望加载的任意路径的 dex/zip/jar/apk 路径,以:分割
      构造方法传入优化缓存路径,需 data/data/packageName/xxx
    • BaseDexClassLoader
      构造方法创建 DexPathList 对象
      DexPathList 对象构造方法获取 Element[]数组
      获取 Element[]数组的过程是将 dex 文件包装成 Element 对象,并将任意位置的 dex/zip/jar/apk 存储到指定的优化缓存路径当中

    • DexClassLoader 继承于 BaseDexClassLoader
      DexClassLoader 并没有重写 BaseDexClassLoader 的 findClass()方法
      DexClassLoader 调用 findClass()
      BaseDexClassLoader 调用 findClass()
      DexPathList 调用 findClass()
      遍历 Element[]数组,根据所传入 name 来加载对应 dex 文件
      找到第一个之后,则直接加载并返回,后续的不再继续进行查找

    • 基于 android 类加载机制实现的热修复
      任意位置存入修复后的 dex/zip/jar/apk,设置优化缓存路径
      创建 DexClassLoader
      构造方法将任意位置存入的修复后的 dex/zip/jar/apk 存入设置的优化缓存路径当中,并包装成 Element[]数组
      此时,我们未进行 findClass(), 修复后的 dex 文件并没有被类加载器加载
      最理想的情况是,在第一次调用出现 bug 之前,findClass()进行修复后的 dex 文件加载替换掉有 bug 的 dex
      我们也不可能去判断哪个时候是第一次加载,然后在其之前调用 DexClassLoader 中的 findClass()来加载,因为当我们上一次发布,即具有 bug 的程序时,是根本不知道这个 bug 的存在的
      那么,换一个角度,当通过类加载器加载我们普通的 apk 中的类的时候,是通过 PathClassLoader 来完成的,那么,只需要修改 PathClassLoader 当中的 Element[]数组即可
      将通过 DexClassLoader 生成的 Element[]数组,放置于 PathClassLoader 的 Element[]之前
      那么,第一次加载 bug 的类的时候,系统通过 PathClassLoader 进行 findClass(),而此时在最前面的是我们在 DexClassLoader 中生成的 Element[]数组,这样,就不会再加载原有的具有 bug 的类了,也就是完成了修复

    示例:
    github示例

    如何生成dex文件

    • 注意:

    当已经通过类加载器加载了具有 bug 的类之后,修复类则不会加载了,所以我们将修复放在尽可能前一些的位置,这也是为什么我们常说的热修复需要重启的原因,可能在我们的修复包发送之前,客户已经启动过我们的应用了,也已经加载过 bug 类,他一直没有退过,则永远不会加载修复类

    就像示例中所展示的,如果先点击了 add,则已经加载过 test 类,再点击就没有作用了
    这时候,只需要把程序退出,然后重新启动,先点击修复,再点击 add,则可以正确执行

    相关文章

      网友评论

        本文标题:Android 类加载机制以及基于类加载机制的热修复

        本文链接:https://www.haomeiwen.com/subject/jcyjuqtx.html