Android类加载机制简析

相关Android源码均基于API 26。

一、Class类简介

​ Class是Java程序在运行时,系统对所有的对象进行所谓的运行时类型标识。它记录了每个对象所属的类。虚拟机使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。Class类封装一个对象和接口运行时的状态,当装载类时,Class类型的对象自动创建。

​ Class 没有公共构造方法。Class 对象是在加载类时由Java 虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。

​ 虚拟机为每种类型管理一个独一无二的Class对象。也就是说,每个类(型)都有一个Class对象。运行程序时,Java虚拟机(JVM)首先检查是否所要加载的类对应的Class对象是否已经加载。如果没有加载,JVM就会根据类名查找.class文件,并将其Class对象载入。

​ 基本的 Java 类型(boolean、byte、char、short、int、long、float 和 double)和关键字 void 也都对应一个 Class 对象。

​ 每个数组属于被映射为 Class 对象的一个类,所有具有相同元素类型和维数的数组都共享该 Class 对象。

二、JVM平台提供的三层classLoader

  1. Bootstrap classLoader:采用native code实现,是JVM的一部分,主要加载JVM自身工作需要的类,如java.lang.、java.uti.等。Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。
  2. ExtClassLoader:扩展的class loader,加载位于$JAVA_HOME/jre/lib/ext目录下的扩展jar。
  3. AppClassLoader:系统class loader,父类是ExtClassLoader,加载$CLASSPATH下的目录和jar;它负责加载应用程序主函数类。

三、Android类加载器种类&分析

Android中有5种类加载器:

  • BootClassLoader

  • URLClassLoader

  • BaseDexClassLoader-----DexClassLoader、PathClassLoader、InMemoryDexClassLoader(O新增)

1. ClassLoader

ClassLoader是所有类加载器的父类,是一个抽象类。

  • ClassLoader有三个构造方法,均是private/protected修饰的,如下:

    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
    }
    
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
    
    //getSystemClassLoader()最终返回的是一个PathClassLoader
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
    }
    

    SystemClassLoader是ClassLoader的一个内部类:

    static private class SystemClassLoader {
            public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }
    
    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");
    
        // String[] paths = classPath.split(":");
        // URL[] urls = new URL[paths.length];
        // for (int i = 0; i < paths.length; i++) {
        // try {
        // urls[i] = new URL("file://" + paths[i]);
        // }
        // catch (Exception ex) {
        // ex.printStackTrace();
        // }
        // }
        //
        // return new java.net.URLClassLoader(urls, null);
    
        // TODO Make this a java.net.URLClassLoader once we have those?
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }
    

    ​ 也就是说,当我们创建一个ClassLoader是,要么显示的指定他的parent加载器,否则会自动创建一PathClassLoader作为parent。

    ​ 这里SystemClassLoader是一个典型的使用静态内部类实现的单例模式。

    在Java中,一个ClassLoader创建时如果没有指定parent,那么它的parent默认是AppClassLoader,AppClassLoader继承自URLClassLoader。

  • ClassLoader的loadClass方法如下:

    public Class loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException{
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

ClassLoader的loadClass方法表现了类加载的基本流程:

  1. findLoadedClass方法检查是否已经加载过这个类;
  2. 如果没有,调用parent的loadClass方法加载该类,如果parent为null,调用findBootstrapClassOrNull方法加载;
  3. 调用parent的loadClass方法后,该class仍为空,则调用findClass方法查找该类(在ClassLoader中发方法抛出ClassNotFoundException异常)。
这也就是我们常说的 “双亲委派模型”———先向上委托父加载器加载class,找不到再向下返回在自己的类路径下查找并加载目标类

2. BootClassLoader

​ BootClassLoader是ClassLoader的内部类,无修饰符修饰(default)。所以我们无法使用BootClassLoader,也不能使用BootClassLoader动态加载类。

  • BootClassLoader的构造方法中传参为null,结合父类ClassLoader的构造方法,我们可以看到BootClassLoader的parent为null,即BootClassLoader是Android最顶级的ClassLoader。
public BootClassLoader() {
    super(null);
}
  • BootClassLoader的loadClass方法如下:
    @Override
    protected Class loadClass(String className, boolean resolve)
           throws ClassNotFoundException {
        Class clazz = findLoadedClass(className);

        if (clazz == null) {
            clazz = findClass(className);
        }

        return clazz;
    }
  • BootClassLoader的findClass方法使用Class.classForName实现查找类:
@Override
protected Class findClass(String name) throws ClassNotFoundException {
    return Class.classForName(name, false, null);
}

​ Class.forName(className)是我们利用反射创建一个类常用的方法,内部实际调用的方法是 Class.forName(className,true,classloader);​第2个boolean参数表示类是否需要初始化, 第三个参数是加载这个类的加载器。

3. URLClassLoader

​ URLClassLoader并不是直接继承ClassLoader,而是继承自SecureClassLoader类。

​ URLClassLoader只能用于加载jar文件,但是由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器。在Java开发中,我们可以利用URLClassLoader读取Jar包并反射类。

4. BaseDexClassLoader

​ BaseDexClassLoader继承自ClassLoader,是DexClassLoader、PathClassLoader、InMemoryDexClassLoader的父类,用于加载各种dex中的类,它的两个构造函数如下:

    //access文件
    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }
    //access path
    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());
        }
    }

它的四个参数:

  • dexPath:指目标类所在的路径(可以加载APK、DEX和JAR,也可以从SD卡进行加载)。如果包含多个路径,则需要用System.getProperty("path.separtor")返回的 分隔符分隔。

  • optimizedDirectory:该文件是dexPath路径中的文件解压、优化生成ODEX文件的文件,ClassLoader只能加载内部存储路径中的dex文件,所以该文件路径必须为内部路径。

  • librarySearchPath:指目标类中所使用的C/C++库存放的路径。

  • parent:该加载器的parent。

BaseDexClassLoader的findClass方法,其主要通过DexPathList.findClasss实现:

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

​ 在构造函数和findClass方法中,都涉及到一个关键类DexPathList。在DexPathList的实现代码内部,会根据类加载器加载路径查找dex文件,然后将它们解析成Element对象,Element对象代表的是dex文件或资源文件,它保存了文件对象。

​ DexPathList类构造的时候会首先将dexPath变量内容分隔成多个文件路径,并且根据路径查找Android中的dex和资源文件,将它们解析后存放到Element数组中。它的构造函数如下:

    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        // 当前类加载器的父类加载器
        this.definingContext = definingContext;
        ArrayList suppressedExceptions = new ArrayList();
        // 根据输入的dexPath创建dex元素对象
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                suppressedExceptions.toArray(newIOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

​ dexElements通过makeDexElements方法获得。makeDexElements把前面dexPath里面解析到的路径下的文件全部遍历一遍,如果是dex文件或apk和jar文件就会查找它们内部的dex文件,将所有这些dex文件都加入到Element数组中,完成加载路径下面的所有dex解析。

private static Element[] makeDexElements(List files, File optimizedDirectory,
            List suppressedExceptions, ClassLoader loader) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * Open all files and load the (direct or contained) dex files up front.
       */
      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();

              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      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) {
                      /*
                       * 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;
    }

DexPathList.findClasss两参方法如下:

    public Class findClass(String name, List suppressed) {
        // 遍历从dexPath查询到的dex和资源Element
        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;
    }

​ 在该方法中查找名称为name的类时,遍历Element数组,找到是dexFile就直接调用DexFile.loadClassBinaryName方法,该方法能够从dex文件数据中生成Class对象。

public Class findClass(String name, ClassLoader definingContext,
               List suppressed) {
            // 使用DexFile.loadClassBinaryName加载类
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed): null;
        }

Element是一个静态内部类,集合了对dex的一些操作

  • 总结---BaseDexClassLoader在实现加载类的流程如下:

    在构造的时候会先将dexPath使用“:”分隔开,然后遍历每个路径下面的所有文件,查找到.dex文件.apk.jar类型的文件并将它们保存在Element数组中,当程序需要加载类的时候会直接遍历所有的Element对象,查找到和dex文件相关的Element就直接加载数据生成Class对象。

5. DexClassLoader

DexClassLoader继承了BaseDexClassLoader,只有一个构造方法,如下所示:

    /**
   * A class loader that loads classes from {@code .jar} and {@code .apk} files
   * containing a {@code classes.dex} entry. This can be used to execute code not
   * installed as part of an application.
   *
   * 

This class loader requires an application-private, writable directory to * cache optimized classes. Use {@code Context.getCodeCacheDir()} to create * such a directory:

   {@code
   *   File dexOutputDir = context.getCodeCacheDir();
   * }
* *

Do not cache optimized classes on external storage. * External storage does not provide access controls necessary to protect your * application from code injection attacks. */ public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), librarySearchPath, parent); } }

所以,DexClassLoader只是做了简单封装,还是基于BaseDexClassLoader实现加载的。

6. PathClassLoader

官方解释入下:

/**
 * Provides a simple {@link ClassLoader} implementation that operates on a list
 * of files and directories in the local file system, but does not attempt to
 * load classes from the network. Android uses this class for its system class
 * loader and for its application class loader(s).
 */

PathClassLoader同样也是对BaseDexClassLoader的简单封装,通过文件list或者文件夹加载class,但是不允许通过网络加载。它有两个构造方法:

参数含义:

  • dexPath:dex路径
  • librarySearchPath:c、c++库路径
  • parent:父加载器
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }

​ 不设置librarySearchPath的构造方法,此时默认路径为/data/dalvik-cache。

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

PathClassLoader在Dalvik虚拟机上只能加载已安装的apk,而在ART上没有此限制。

7. InMemoryDexClassLoader

官方解释如下:

21/**
22 * A {@link ClassLoader} implementation that loads classes from a
23 * buffer containing a DEX file. This can be used to execute code that
24 * has not been written to the local file system.
25 */

InMemoryDexClassLoader是Android O新增的一个类加载器,也是继承自BaseDexClassLoader。它提供了从内存中的dex文件加载class的能力。

构造函数如下:

    //Create an in-memory DEX class loader with the given dex buffers.
    public InMemoryDexClassLoader(ByteBuffer[] dexBuffers, ClassLoader parent) {
        super(dexBuffers, parent);
    }

该方法通过传入一个ByteBuffer[],加载内存中的dex文件中的类。

另一个构造方法是传入一个ByteBuffer,大同小异:

    public InMemoryDexClassLoader(ByteBuffer dexBuffer, ClassLoader parent) {
        this(new ByteBuffer[] { dexBuffer }, parent);
    }

你可能感兴趣的:(Android类加载机制简析)