认识 ClassLoader
} 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;
}
这个方法就是类加载的核心方法,我们来看下它是如何实现的?
首先它会**通过 Class<?> c = findLoadedClass(name);**来判断自身是否加载过入参为 name 的类文件,如果没有加载过,就继续判断 parent ClassLoader 是否加载过这个类文件,如果都没有加载过,判断 c==null 说明这个类从来没有被加载过,那么就需要当前的 ClassLoader 去调用它的 findClass
去查找这个类,查找到之后就 return 回去,所以这个双亲委托模型其实很简单,就是首先看自己是否加载过,没有的话就看父 ClassLoader 是否加载过,这个其实就是双亲委托模式的核心。由于都没有加载过的时候会调用 findClass 方法到 Dex 文件中查找这个类,所以我们接下来就看一下
findClass 内部是如何实现的,它是如何一步步找到这个类的呢?我们跟进这个方法:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以发现这个方法是一个空实现,没有任何的行为,那么接下来该怎么看呢?其实稍微思考一下你就能想明白了,很简单就是交给它的子类去实现,这样子类就可以定义它们不同的查找行为。那么 ClassLoader 有哪些子类呢?
上面都已经介绍到了,即:BaseDexClassLoader、DexClassLoader、PathClassLoader。所以接下来,就来看一下这几个类的源码又是如何实现的?不幸的是在我们的 AndroidStudio 中这几个类的源码是无法阅读的,所以这个解决办法就是八仙过海各显神通了,你可以去下载一个版本的 Android 系统源码到自己的电脑上,然后通过搜索类名查找到对应的类去阅读,这里推荐一个软件 Source Insight,用这个软件进行阅读源码很方便哦,具体怎么使用请自行谷歌或百度。我这里使用的方式是在线上查看,不想下载的可以直接使用我这种方式,这里也把网站地址给大家:https://www.androidos.net.cn/sourcecode,站内 Android 系统源码和 Linux Kernel 源码都有,大家可以根据自己的需要查看相关源码。
3.2、DexClassLoader 源码解析
package dalvik.system;
/**
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>Prior to API level 26, 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 {
/**
Creates a {@code DexClassLoader} that finds interpreted and native
code. Interpreted classes are found in a set of DEX files contained
in Jar or APK files.
<p>The path lists are separated using the character specified by the
{@code path.separator} system property, which defaults to {@code :}.
@param dexPath the list of jar/apk files containing classes and
@param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
@param librarySearchPath the list of directories containing native
@param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
DexClassLoader 的实现非常简单,可以看到它只有一个构造方法,这个方法的入参有四个:
第一个是指定要加载的 Dex 文件的路径
第二个是指定的这个 Dex 文件要被 copy 到哪个路径中,这个一般是应用程序内部路径
第三个是 librarySearchPath,包含本地目录的列表库
第四个就是父 ClassLoader
从这个类开始的注释中可以看到,它说这个 ClassLoader 可以用来加载一些来自于 jar 包和 apk 中包含的 class.dex 的类,它可以加载一些并没有被安装到应用中的类,所以说 DexClassLoader 才是动态加载的核心。
3.3、PathClassLoader 源码解析
package dalvik.system;
/**
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).
*/
public class PathClassLoader extends BaseDexClassLoader {
/**
Creates a {@code PathClassLoader} that operates on a given list of files
and directories. This method is equivalent to calling
{@link #PathClassLoader(String, String, ClassLoader)} with a
{@code null} value for the second argument (see description there).
@param dexPath the list of jar/apk files containing classes and
resources, delimited by {@code File.pathSeparator}, which
defaults to {@code ":"} on Android
@param parent the parent class loader
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
/**
Creates a {@code PathClassLoader} that operates on two given
lists of files and directories. The entries of the first list
should be one of the following:
<ul>
<li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
well as arbitrary resources.
<li>Raw ".dex" files (not inside a zip file).
</ul>
The entries of the second list should be directories containing
native library files.
@param dexPath the list of jar/apk files containing classes and
resources, delimited by {@code File.pathSeparator}, which
defaults to {@code ":"} on Android
@param librarySearchPath the list of directories containing native
libraries, delimited by {@code File.pathSeparator}; may be
{@code null}
@param parent the parent class loader
*/
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
PathClassLoader 的源码同样很简单,它主要是有两个构造方法,我们重点来看下第二个构造方法,这个方法与 DexClassLoader 的唯一区别就是少了要拷贝到的内部文件路径,正是由于缺少这个路径,所以 PathClassLoader 只能加载已经安装到系统中的 APK 的 Dex 文件。通过上面的分析可以发现,DexClassLoader 和 PathClassLoader 其实没有任何的作用,唯一的区别就是前者可以加载指定路径下的 Dex 文件,而后者只能加载已经安装到系统的 Dex 文件。它们真正的行为其实都是在它们的父类 BaseDexClassLoader 中去完成的,下面就来看下 BaseClassLoader 具体是如何完成类的查找的。
3.4、BaseDexClassLoader 源码解析
这个类的源码相对来说长一些,我就不贴源码了,带着看一下几个方法吧。
首先通过浏览可以发现这个类的核心方法就是 findClass(),也就是继承自 ClassLoader 的那个 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.findClass(name,suppressedExceptions)**通过传入要查找的类名来真正完成 Class 字节码文件的查找,其它的都是一些异常的处理,所以 BaseClassLoader 也只是一个中转并不是真正去查找类的
地方。
接着来看它的构造方法:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
它在构造方法中初始化了 pathList,通过 new DexPathList 进行初始化,初始化的时候首先传入了 this,this 也就是 ClassLoader,然后是 dexPath 就是我们要加载的 dex 文件的路径。所以我们需要继续跟进 DexPathList 类。
3.5、DexPathList 源码解析
首先来看它的几个重要的成员变量,这里都加了注释:
//要加载的文件都是.dex 后缀
private static final String DEX_SUFFIX = ".dex";
//这个就是在构造方法中传入的
private final ClassLoader definingContext;
//DexPathList 的内部类
private Element[] dexElements;
这个 Element 内部类的主要的一个成员变量就是 DexFile,也就是 Dex 文件在 Android 虚拟机中的具体实现。
private final DexFile dexFile;
接着来看它的构造方法,构造方法中主要就是初始化刚刚说的一些成员变量,重点来看这一行,其它的 Elements 都是和这个类似的:
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
这个 dexElements 是通过 makeDexElements 这个内部方法来初始化这个 dexElements 数组,我们跟进这个方法会发现最后又是通过 makeElements 这个方法来实现的,我们跟到这个方法里面:
private static Element[] makeElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions,
boolean ignoreDexFiles,
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) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
zip = new File(split[0]);
dir = new File(split[1]);
} else if (file.isDirectory()) {
// We support directories for looking up resources and native libraries.
// Looking up resources in directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file, true, null, null);
} else if (file.isFile()) {
if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
zip = file;
if (!ignoreDexFiles) {
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);
}
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
elements[elementsPos++] = new Element(dir, false, zip, dex);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
它内部是遍历所有的 File 就是所有的 Dex 文件,如果是文件夹的话会继续往内部去递归,如果是文件并且 name 是以.dex 为后缀的,如果是则表明这个文件就是真正要加载的 dex 文件,它就会通过 loadDexFile()去创建一个 dex,这个 dex 就是成员变量 dexFile。如果是文件并且是一个压缩文件的话,
同样会通过**loadDexFile()**去获取到内部真正的 DexFile,所以接下来是走到了 loadDexFile()这个方法里面:
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}
这个方法内部先判断可复制的文件夹是否为空,如果为空表明确实就是一个 Dex 文件,则直接 new 一个 DexFile,否则会调用 DexFile 的 loadDex 将它解压等获取到内部真正的 DexFile,所以 makeElements 的核心作用就是将我们指定路径中的所有文件转化成 DexFile 同时存到 Elements 数组中,那么
这样做有什么作用呢?作用就在 findClass 这个方法中实现的,下面就来看下这个最重要的方法 findClass()是如何实现的?
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;
}
这个方法内部是遍历上面通过 makeElements()方法初始化好的 dexElements 数组,拿到里面每一个 DexFile,通过 DexFile 的**loadClassBinaryName()**这个方法去真正的找到 Class 字节码。
总结:通过层层跟踪发现 DexClassLoader 和 PathClassLoader 的所有的功能实现都是在 BaseDexClassLoader 中,而 BaseDexClassLoader 最核心的 findClass()方法又是调用 DexPathList 中的 findClass(),而 DexPathList 中的 findClass()最终又是调用 DexFile 类中的 loadClassBinaryName()去完成的。
接下来我们跟到 DexFile 这个类中看一下:
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
它的最核心的方法 loadClassBinaryName()最终调用的是 defineClass,defineClass 是一个私有类型的静态内部类,最终的逻辑都是在这个类中完成的:
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);
}
评论