写点什么

认识 ClassLoader

用户头像
Android架构
关注
发布于: 8 小时前

} 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 也只是一个中转并不是真正去查找类的


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


地方。


接着来看它的构造方法:


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);


}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
认识ClassLoader