欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android类加载机制简析

程序员文章站 2022-07-14 12:20:15
...

相关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<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;
    }

​ 在构造函数和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<IOException> suppressedExceptions = new ArrayList<IOException>();
        // 根据输入的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<File> files, File optimizedDirectory,
            List<IOException> 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<Throwable> 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<Throwable> 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.
   *
   * <p>This class loader requires an application-private, writable directory to
   * cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
   * such a directory: <pre>   {@code
   *   File dexOutputDir = context.getCodeCacheDir();
   * }</pre>
   *
   * <p><strong>Do not cache optimized classes on external storage.</strong>
   * 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);
    }