美文网首页
热修复Hotfix系列(1)—ClassLoader: 叫爷爷!

热修复Hotfix系列(1)—ClassLoader: 叫爷爷!

作者: 小猪儿粑粑 | 来源:发表于2019-07-11 19:23 被阅读0次

    0x01 前言

    Android上所有的动态加载方案,包括热部署,热修复,插件化都是以ClassLoader作为基础来实现的。Java 代码都是写在 Class 里面的,程序运行在虚拟机上时,虚拟机需要把需要的 Class 加载进来才能创建实例对象并工作,而完成这一个加载工作的角色就是 ClassLoader。一个类在虚拟机上是如何被加载出来的?如何通过ClassLoader来实现动态加载?这是本次我们要探讨的

    ClassLoader是java的一个重要概念,用于加载jar或者class文件,对于android而言,我们所编写的类的存在形式与表现形式都变成了dex,无论是DVM还是ART它们加载的不再是Class文件,而是dex文件。所以我们在学习ClassLoader的时候要区分开两者的异同点,比如java&android中ClassLoader的类型是不同的,但是都可以分为系统ClassLoader与自定义的ClassLoader,类加载时也同样都是通过双亲委托机制来实现的。

    0x02 Android中的ClassLoader

    Android系统ClassLoader主要有3种分别是BootClassLoader、PathClassLoader和DexClassLoader,这三者的关系分别是下面介个亚子的:

    ClassLoader分类.png

    看看红色方框的类,我特别标记的,好好看,好好学~

    BootClassLoader

    class BootClassLoader extends ClassLoader {
        private static BootClassLoader instance;
        @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
        public static synchronized BootClassLoader getInstance() {
            if (instance == null) {
                instance = new BootClassLoader();
            }
            return instance;
        }
    ...
    }
    

    ClassLoader的内部类,并继承自ClassLoader。Android系统启动时会使用BootClassLoader来预加载常用类,用java实现Framework层的字节码文件加载。感兴趣的可以参考一下BootClassLoader的源码ClassLoader.java

    PathClassLoader

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

    PathClassLoader的实现都在BaseDexClassLoader 中,这里先按下不表,接着看DexClassLoader。

    DexClassLoader

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

    是不是感觉被耍了,但是!还是有点东西可以看一下的,就是构造函数的四个参数分别是什么意思,既然这两个东西的具体实现都在BaseDexClassLoader中,那肯定不同的点就是在构造方法里面了,细致,耐心,负责的来看待这几个鸡儿参数~

    PathClassLoader的参数:
    dexPath:dex文件以及包含dex的apk文件或jar文件的路径集合,多个路径用文件分隔符分隔,默认文件分隔符为‘:’。
    librarySearchPath:所使用到的C/C++库存放的路径
    parent:该ClassLoader所对应的父ClassLoader

    DexClassLoader的构造参数:
    optimizedDirectory:Android系统将dex文件进行优化后所生成的ODEX文件的存放路径,一般情况下使用当前应用程序的私有路径:/data/data/<Package Name>/...。
    其他与PathClassLoader相同~~

    中场暂停强行解释一波

    对于这个上面介绍的这两个ClassLoader的异同只存在于这个optimizedDirectory参数,PathClassLoader默认设置为null,是因为使用默认路径“/data/dalvik-cache”,这个目录是apk安装时候对dex包进行odex优化之后的存储目录,而DexClassLoader则可以通过这个参数指定ODEX优化文件的存放路径。那这个造成的区别就是PathClassLoader只可以用来加载已经安装完成的apk中的dex【odex】文件而DexClassLoader则可以用来加载没有安装的apk的dex文件。所以动态加载的核心就是DexClassLoader,我们可以用它来加载一个Dex(热修复),用它来加载一个未安装的apk中的dex(插件化),从而实现对本工程外其他类的加载。

    通过网上其他小伙伴进行的ClassLoader日志输出验证,PathClassLoader的parent为BootClassLoader,同时还验证了应用程序的ClassLoader为PathClassLoader,FrameWork层的ClassLoader为BootClassLoader。这个很重要,好好看,好好学~

    BaseDexClassLoader

    接着上面说的,这个ClassLoader也是直接继承于ClassLoader,并且上面提到的PathClassLoader跟DexClassLoader的具体实现都是在他这,所以这个玩意才是boss,上源码BaseDexClassLoader.java,类的加载都是通过BaseDexClassLoader#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;
        }
    

    这个pathList,emmm..很重要,这个东西,看看是什么

    private final DexPathList pathList;
    
        public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
            super(parent);
            this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
    
            if (reporter != null) {
                reporter.report(this.pathList.getDexPaths());
            }
        }
    

    这个pathList,是一个DexPathList的类型的变量并且在BaseDexClassLoader的构造方法里面就给他*出来了,我这里找到了源码DexPathList.java,大家可以看一下,当然不看也没什么关系,毕竟很多事情我们只要宏观把控就行了,大家都很忙,看看代码片段就完事了~

     /**
         * List of dex/resource (class path) elements.
         * Should be called pathElements, but the Facebook app uses reflection
         * to modify 'dexElements' (http://b/7726934).
         */
        private Element[] dexElements;//Dex文件包装类组成的数组
    

    对于DexPathList里面这个私有变量,解释一下,上面说到我们有个目录,目录里面有n个dex,这些dex是怎么来的呢?就是你apk安装的时候经过优化生成在这个目录下的。那这个东东做的事,就是把你这个dex再经过一次处理,生成为一个DexFile数据结构,然后组成为一个数组放在他自己dexElements里面。那这么猥琐的做法是为什么呢?

    来看看它怎么造作这批dex文件的~

    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
                List<IOException> suppressedExceptions, ClassLoader loader) {
          Element[] elements = new Element[files.size()];
          int elementsPos = 0;
          /*
           * 遍历所有的dex文件
           */
          for (File file : files) {
              if (file.isDirectory()) {    //判断file是否为文件夹
                  elements[elementsPos++] = new Element(file);
              } else if (file.isFile()) {          //判断file是否为文件
                  //获取文件名称
                  String name = file.getName();
                  //判断文件名称是否以“.dex”结尾
                  if (name.endsWith(DEX_SUFFIX)) {
                      // Raw dex file (not inside a zip/jar).
                      try {
                          //将dex文件转换为DexFile对象
                          DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
                          if (dex != null) {
                              //创建Element对象,将DexFile对象作为参数传入,
                              //并将该Element对象添加到elements数组中
                              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) {
                          /*
                           * 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);
                      }
    
                      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;
        }
    

    makeDexElements方法的主要作用为:遍历指定路径下的所有文件,将其中的.dex文件转换成DexFile对象,最终存储到elements数组中。这个造作的过程对我们执行findClass有什么关联,最后你所需要的类是怎么被load出来的,接着回到findClass这个方法

    public Class<?> findClass(String name, List<Throwable> suppressed) {
            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;
        }
    

    DexPathList:老子要给你生成类用的呀!emmmm..... 他findClass的时候要遍历一下这个DexFile数组,看看类存在于哪个dex包装体,给他造出来就完事了。你看看最后我element的findClass方法吧!

    public Class<?> findClass(String name, ClassLoader definingContext,
                    List<Throwable> suppressed) {
                return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                        : null;
            }
    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
            return defineClass(name, loader, mCookie, this, suppressed);
        }
    
     ...
    
     private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                         DexFile dexFile, List<Throwable> suppressed) {
            Class result = null;
            try {
                result = defineClassNative(name, loader, cookie, dexFile);
            } catch (NoClassDefFoundError e) {
                if (suppressed != null) {
                    suppressed.add(e);
                }
            } catch (ClassNotFoundException e) {
                if (suppressed != null) {
                    suppressed.add(e);
                }
            }
            return result;
        }
    
      ...
    
      private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
                                                      DexFile dexFile)
    

    搞了很久,写的我好累,终于到了native方法进行类加载的代码,那我们接下来看看native方法defineClassNative做了什么东西~

    开玩笑,我怎么可能继续再跟下去!老子不会了!

    相关文章

      网友评论

          本文标题:热修复Hotfix系列(1)—ClassLoader: 叫爷爷!

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