目录
写在前面
今天继续《热修复与插件化》专题的分享,专栏的上一篇是说到了JVM的一些基础知识点,本篇接着来说一下ClassLoader的相关内容,当然这里说的是Android中的ClassLoader哈,上一篇《带你认识JVM》中我们已经介绍过了Java中的类加载器、它们各自的作用以及加载流程,这里就不再说了,有需要的自行查看,接下来就直奔主题了,去看Android的类加载机制!
一、androidClassLoader基本介绍
1.1、种类详解
- BootClassLoader:它的作用和Java中的Bootstrap ClassLoader的作用是基本一样的,它主要是用来加载Android Framework层的一些字节码文件。
- PathClassLoader:它的作用和Java中的App ClassLoader的作用是基本类似的,它是用来加载已经安装到系统中的apk文件的class文件。
- DexClassLoader:它的作用和Java中的Custom ClassLoader的作用基本一样,是用来加载指定目录中的class字节码文件。
- BaseDexClassLoader:它是一个父类,PathClassLoader和DexClassLoader是它的两个子类。
通过上面的了解,我们可以发现Android中的ClassLoader在种类上与Java中的ClassLoader基本是一一对应的,只不过是它内部的实现有所不同。
1.2、问题分析
一个APP至少需要哪些ClassLoader才能正常运行呢?。。。。。。。。。。。。。。。。。。。。。。。哈哈,我也不知道!
其实应该是至少需要BootClassLoader和PathClassLoader,我们可以通过一段代码来实际测试看一下:
package com.jarchie.mvc.controller;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.jarchie.mvc.R;
/**
* 作者: 乔布奇
* 日期: 2020-04-07 22:29
* 邮箱: jarchie520@gmail.com
* 描述: ClassLoader测试
*/
public class ClassLoaderTestActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_class_loader);
//获取当前应用的ClassLoader
ClassLoader classLoader = getClassLoader();
if (classLoader != null) {
Log.i("Jarchie-Current", "classLoader:" + classLoader.toString());
//遍历父ClassLoader
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
Log.i("Jarchie-Parent", "classLoader:" + classLoader.toString());
}
}
}
}
来看下结果,和我们设想的是一样的:
二、androidClassLoader的特点及作用
2.1、特点
- 双亲代理模型
ClassLoader在加载一个字节码的时候,首先会询问当前的ClassLoader是否已加载过此类,若已经加载过则直接返回,不再重复加载,若没有加载过,它会查询它的父ClassLoader是否已经加载过此类,若加载过则直接返回父ClassLoader加载过的字节码文件,若整个继承线路上的ClassLoader都没有加载过,那么最终会由它的子ClassLoader去完成真正的加载。这样做有一个明显的特点,如果一个类被位于树中的任意一个ClassLoader节点加载过,那么在以后的整个系统的生命周期中这个类都不会再被重新的加载,极大的提高了加载类的效率。
2.2、作用
- 类加载的共享功能:一些Framework层级的类一旦被顶层的ClassLoader加载过,那么它就会缓存在内存中,以后任何地方用到,都无需重新加载。
- 类加载的隔离功能:不同继承路线上的ClassLoader加载的类肯定不是同一个类,这样避免了开发者写一些代码冒充核心的类库去访问核心类库中一些可见的成员变量。举个栗子:一些系统层级的类会在初始化的时候被加载,比如java.lang.String,这个是在应用程序启动之前就被系统加载好的,如果在一个应用中能够使用自定义的String类去把系统的String类替换掉的话,就会产生严重的安全问题。所以判定两个类是同一个类的必要条件是:相同的ClassName、相同的packageName、同一个ClassLoader加载的。
三、ClassLoader源码解析
3.1、ClassLoader源码解析
这一部分我们来看下ClassLoader的源码,通过源码我们来看下代码里面是如何实现双亲委托模型的。
OK,现在我们打开AndroidStudio,按住Ctrl键点击ClassLoader进到源码中去,首先找到这个类里面最核心的方法loadClass:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
可以看到这个方法内部最终是调用的两个参数的loadClass方法,所以接着点进去跟到这个方法里面去:
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;
}
这个方法就是类加载的核心方法,我们来看下它是如何实现的?
首先它会通过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
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
* @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 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);
}
}
return result;
}
可以看到这个类中除了一些异常的捕获之外,只有一句有意义的代码就是它最终又是通过defineClassNative()这个方法去完成类的查找的,我们继续跟进这个方法:
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)
throws ClassNotFoundException, NoClassDefFoundError;
可以看到这个方法是一个native类型的方法,也就是说它内部的源码我们就无法再继续往下查看了,因为内部都是C/C++的代码了,那到这里我们源码的跟读就结束了,其实复杂的东西都写在C层了,我们能看到的无非就是嵌套的层级多一些,其实复杂度倒是没多少。
四、ClassLoader加载流程分析
这一部分为了加深印象所以再来回顾一下ClassLoader的整个加载流程,首先看下图:
从上图可以看到类的加载都是在ClassLoader中的loadClass()方法中完成的,它会首先判断类是否已经被自己或者双亲加载过,如果加载过,就直接使用这个类,如果没有加载过,它会调用ClassLoader中的findClass()方法去寻找这个类,寻找的过程又会调用BaseDexClassLoader中的findClass()方法去寻找,而这个findClass()方法其实又是调用了DexPathList类中的findClass()方法,同时DexPathList这个类还将所有的Dex文件都转化成DexFile,并且通过它的makeElements()方法把它们转化成一个Elements数组,然后findClass()方法通过遍历这个数组找到其中的每一个DexFile,然后调用每一个DexFile中的loadClassBinaryName()方法去完成每一个Class的寻找,loadClassBinaryName()方法最终又通过defineClassNative()这个native方法完成真正的类的查找,以上就是整个ClassLoader它的load和find的过程。
五、动态加载难点
- 有许多组件类需要注册才能使用:做过Android开发的都知道,在Android开发的过程中,它的四大组件是需要在Manifest中注册以后才可以正常工作的,也就是说,即使你动态加载一个组件进来,如果这个组件没有注册的话还是无法工作的。
- 资源的动态加载很复杂:资源是我们Android开发中经常用到的,而Android是把资源用对应的id注册好,在运行的时候通过这些id从Resource实例中获取对应的资源,所以如果是运行时动态加载进来的新类里面用到的那些资源就会抛出找不到这个异常,因为新类的id和现有的Resource中保存的资源id对不上,总结一句话就是资源也需要向系统去注册。
- Android的各个版本对类和资源的加载方式可能各有不同:即:适配上比较复杂。
这些难点归根到底可以用一句话总结:Android程序运行需要一个上下文环境。
写到这里关于ClassLoader的相关内容就说的差不多了,这也是这个专栏的理论知识的最后一篇了,下一篇准备写点实际操作的东西了,这个东西也是大家经常听到的一个词——热修复,不过可能会晚一些,尽量会在月底之前发出来,加油吧,骚年!
温馨链接——本专栏文章速查:
第二篇:《带你认识JVM》