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
- Bootstrap classLoader:采用native code实现,是JVM的一部分,主要加载JVM自身工作需要的类,如java.lang.、java.uti.等。Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。
- ExtClassLoader:扩展的class loader,加载位于$JAVA_HOME/jre/lib/ext目录下的扩展jar。
- 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方法表现了类加载的基本流程:
- findLoadedClass方法检查是否已经加载过这个类;
- 如果没有,调用parent的loadClass方法加载该类,如果parent为null,调用findBootstrapClassOrNull方法加载;
- 调用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);
}