其他有关插件化的文章欢迎大家观阅
插件化踩坑之路——Small和Atlas方案对比
Android插件化基础篇—— class 文件
Android插件化基础篇 — dex 文件
Android 插件化基础——虚拟机
Android 和 Java 平台的类加载平台区别较大,是我们基础篇的重点,我们将从三个方面来讲解 ClassLoader。
Java 中的 ClassLoader 回顾
之前的文章中,我们已经看过这张图了,那篇文章中也简单的讲解了类的加载流程,加载流程两个平台差不多,如何大家还不太熟悉可以去上面给出的虚拟机文章中再复习一下。
Android 中的 ClassLoader 详解
Android 中的 ClassLoader 种类
Android 中的 ClassLoader 有以下几种类型:
- BootClassLoader
- PathClassLoader
- DexClassLoader
- BaseDexClassLoader
BootClassLoader 作用和 Java 中的 Bootstrap ClassLoader 作用是类似的,是用来加载 Framework 层的字节码文件的。
PathClassLoader 作用和 Java 中的 App ClassLoader 作用有点类似,用来加载已经安装到系统中的 APK 文件中的 Class 文件。
DexClassLoader 和 Java 中的 Custom ClassLoader 作用类似,用来加载指定目录中的字节码文件。
BaseDexClassLoader 是一个父类,DexClassLoader 和 PathClassLoader 都是它的子类。
一个 App 至少需要 BootClassLoader 和 PathClassLoader 才能运行。为了证明这一点,我们写一个简单的页面,在 MainActivity
的 onCreate()
方法中写下如下代码:
ClassLoader classLoader = getClassLoader();
if (classLoader != null) {
Log.e("weaponzhi", "classLoader: " + classLoader.toString());
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
Log.e("weaponzhi","classLoader: "+classLoader.toString());
}
}
最后我们发现输出dalvik.system.PathClassLoader
和java.lang.BootClassLoader
。当然不同机子可能输出的结果不同,但至少会有这两个 ClassLoader。BootClassLoader 负责加载 framework 字节码文件,所以每个应用都是需要的,而 PathClassLoader 用来加载已安装 Apk 的字节码文件,这些东西都是一个应用启动的必要东西。
Android 中 ClassLoader 特点及作用
Android 中的 ClassLoader 最大的特点就是双亲代理模型。双亲代理模型主要分三个过程:在加载字节码的时候,会询问当前 ClassLoader 是否已经加载过,如果加载过则直接返回,不再重复加载,如果没有的话,会查询 parent 是否加载过,如果加载过,就直接返回 parent 加载的字节码文件。如果整个继承线路上的 ClassLoader 都没有加载,执行类才会由当前 ClassLoader 类进行真正加载。
这样做的好处是,如果一个类被位于树中任意 ClassLoader 节点加载过,那么以后整个系统生命周期中,这个类都将不会被加载,大大提高了加载类的效率。由于这样的特点,就给我们 ClassLoader 带来了两个作用。
第一个作用就是类加载的共享功能。当一个 framework 层中的类被顶层 ClassLoader 加载过,那么这个类就会被缓存在内存里,以后任何需要用到底地方都不会重新加载了。
第二个作用就是类加载的隔离功能。不同继承路线上的 ClassLoader 加载的类肯定不是同一个类,这样就有一定的安全性,避免了用户自己写一些代码冒充核心类库来访问这些类库中核心代码和变量。
所以如何判断两个类是同一个类呢,不仅需要工程中的包名类名一致,还需要由同一个 ClassLoader 加载的,这三条同时满足才能说是一个类。
Android ClassLoader 源码讲解
我们下面就来通过源码来看看 Android ClassLoader 到底是如何实现双亲代理模式的。
首先我们进入 ClassLoader.java 这个类,查找它最核心的方法 loadClass()
看看它是怎么实现的
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 1.查看 class 是否已经被加载过
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//2.如果没有被加载过,则判断 parent ClassLoader 有没有加载过
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
}
//3.如果类没有被加载过,那么就通过当前 ClassLoader 来加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
我在代码中注释已经比较清楚了,源码中首先会判断当前的 ClassLoader 有没有加载过这个类,如果没有加载过,再会看看 parent ClassLoader 有没有加载过,如果整个继承线路走过后 class 依然为 null,则再回到当前 ClassLoader 通过 findClass()
方法来加载 class。
好,现在让我们继续跟踪 findClass()
方法,进去后发现这个方法是个空实现,说明真正的实现代码都在 ClassLoader 的子类中实现,我们在 Android Studio 中,查找类似 PathClassLoader 这样的类是无法看到代码的,所以我们可以通过源码网站 AndroidXRef 或者其他观看源码的方式来查看下 Android 几个 ClassLoader 的具体实现。
打开 DexClassLoader
发现很简单,类中只有一个构造方法,继承自 BaseDexClassLoader
,下面我们来看看这个构造方法。
public DexClassLoader(String dexPath, String optimizedDirectory,String libraryPath,ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
参数dexpath
指定我们要加载的 dex 文件路径,optimizedDirectory
指定该 dex 文件要被拷贝到哪个路径中,一般是应用程序内部路径。
DexClassLoader
类上有一段官方注释:
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.
这段注释的意思就是,DexClassLoader 可以加载一些 jar 包和 apk 包里面的 dex 文件,可以用来加载一些并没有安装到系统应用中的类。所以,DexClassLoader 是动态加载的核心。
下面我们再来看看 PathClassLoader 是如何实现的,它同样也是继承于 BaseDexClassLoader,并且也重写了构造方法。
public PathClassLoader(String dexPath, String libraryPath,ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
我们可以看到,它和 DexClassLoader 的区别就在于少了一个 optimizedDirectory 的参数,所以 PathClassLoader 没有办法加载没有安装到系统中的应用的类。
我们发现,这两个 ClassLoader 并没有什么具体实现,真正的实现都是在他们的父类 BaseDexClassLoader
中,所以我们下面看一下它的实现。
public class BaseDexClassLoader extends ClassLoader{
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath,File optimizedDirectory,
String libraryPath,ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this,dexPath,libraryPath,optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throw ClassNotFoundException{
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name,suppressedExceptions);
if (c == null){
ClassNotFoundException cnfe = new ClassNorFoundException("xxx");
for (Throwable t : suppressedExceptions){
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
}
我们通过构造方法可以观察到,如果 optimizedDirectory 为空,那么代表这是 PathClassLoader,不为空则是 DexClassLoader,findClass()
方法虽然我们终于看到了实现,但发现真正的实现还没有在这里,而是在 DexPathList
对象的findClass()
方法中,不要气馁,结果就在前方,我们继续跟进!
DexPathList
这个类代码比较多,我们来从它的成员变量中开始,挑重点看。
final class DexPathList{
private static final String DEX_SUFFIX = ".dex";
private final ClassLoader definingContext;
private final Element[] dexElements;
...
public DexPathList(ClassLoader definingContext,
String dexPath,String libraryPath,File optimizedDirectory){
...
this.dexElements = makeDexElements(splitDexPath(dexPath),optimizedDirectory,suppressedException);
...
}
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;
}
}
}
}
}
我们关注几个点,一个是 DEX_SUFFIX
这个成员变量,代表 dex 文件后缀,方便后面的一些文件处理判断使用。 definingContext
就是在初始化的时候传进来的 ClassLoader,dexElements
DexPathList 中一个静态内部类对象数组,在构造方法中初始化,这个对象数组是 findClass()
的关键参数,通过遍历获取 Elements 中的 DexFile 对象,调用 DexFile 的 loadClassBinaryName()
方法,完成 class 文件的获取。
static class Element{
private final File file;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
public Element(File file,boolean isDirectory,File zip,DexFile dexFile){
this.dir = dir;
this.isDirectory = isDirectory;
this.zip = zip;
this.dexFIle = dexFIle;
}
}
Element 就是 dexElements 对象数组存储的具体静态内部类,该类我只是简单列举下它的成员变量。dexElements 在 DexPathList 的构造方法中初始化,我们来细致的看下 makeDexElements
方法,该方法直接指向 makeElements()
方法,源码如下:
private static Element[] makeElements(List<File> files,File optimizedDirectory,
List<IOException> suppressedExceptions,
boolean ignoreDexFiles,
ClassLoader loader){
Element[] elements = new Element[file.size()];
int elementsPos = 0;
for (File file : files){
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
//1
if (path.contains(zipSeparator)){
...
//2
}else if(file.isDirectory()){
elements[elementsPos++] == new Element(file,true,null,null);
//3
}else if (file.isFile()){
//4
if(!ignoreDexFiles && name.endsWith(DEX_SUFFIX)){
dex = loadDexFile(file,optimizedDirectory,loader,elements);
//5
}else{
zip = file;
//6
if(!ignoreDexFiles){
dex = loadDexFile(file,optimizedDirectory,loader,elements);
}
}
}
}
}
这里我省略掉了一些代码,只看重点。其中注释中第一个和第二个 if 语句中的代码的作用是如果路径是文件夹的话,就继续向下递归,第三个判断是否是文件,如果是,进入第四个,判断文件是否是以 .dex
为后缀的,如果是的话标明这个文件就是我们需要加载的 dex 文件,通过 loadDexFile()
方法来加载 DexFile 对象。如果是文件,并且是个压缩文件的话,就会进入第五个 if 语句中,同样会通过 loadDexFile()
来进行 DexFile 加载。下面来看一下 loadDexFile()
方法实现。
private static DexFile loadDexFile(File file,File optimizedDirectory,Classloader loader,
Element[] elements) throw IOException{
if(optimizedDirectory == null){
return new DexFile(file,loader,elements);
}else{
String optimizedPath = optimizedPathFor(file,optimizedDirectory);
}
}
如果optimizedDirectory
为空,说明文件就是 dex 文件,那么直接创建 DexFile 对象即可,如果不为空,则调用 loadDex() 方法,将它解压然后获取内部真正的 DexFile。所以 makeElements() 就是通过文件获取 dex 文件,转化为 Elements 对象数组,然后给findClass()
方法使用。
loadClassBinaryName()
方法再往下走就是 native 方法了,我们就无法继续看了,大概可以想像这个 native 方法就是通过 C、C++去查找 dex 指定 name 相关的东西,然后将它拼成 class 字节码,最后返回给我们。
整体的源码我们大概就看过了,实际上不是很复杂,只是嵌套很多,真正复杂的地方都在 native 中了,所以我们看源码一定要耐心细心,不能惧怕,看不懂就多看几遍,学习一下他们的编程思路和设计思想,对我们能力提高有极大帮助。
Android 中的动态加载比 Java 程序复杂在哪里
Android 中的动态加载在我们之前源码分析之后,感觉看起来不是很复杂,只要利用好几个 ClassLoader ,整体的思路还是比较清晰的,但在实际设计的时候远远没有那么简单,主要是因为 Android 有他的复杂性:
- 有许多组件类,比如四大组件,都是需要注册才能使用的。需要在 AndoridManifest 注册才能使用。
- 资源的动态加载非常复杂。Android 的资源很特殊,都是通过 id 注册的,通过 id 从 Resource 实例中获取对应的资源,如果是动态加载的新类,资源 id 就会找不到,总而言之就是资源也是需要动态注册的。
- Android 每个版本对于类和资源加载的方式都是不同的,适配也是一个极为头疼的问题。
以上难点总结起来可以用一句话概括:「Android 程序运行需要一个上下文环境」。上下文环境可以给组件提供需要的功能,比如主题、资源、查询组件等。那么我们如何给动态加载的组件和类提供上下文环境呢,其实这就是第三方动态加载库主要解决的问题,也是非常复杂的,像 Tinker 和 Atlas 这些比较成熟的动态加载方案都是以解决这些问题作为核心而设计的,我们个人要解决可能比较困难,但我们可以通过使用和阅读源码,来学习他们的实现原理,大致了解即可。
下一篇文章我们将利用我们学到的 ClassLoader 相关知识,自己尝试写一个简单的插件加载 demo 和插件管理器。
本文部分内容参考于慕课网实战课程「Android 应用发展趋势必备武器 热修复与插件化」,有兴趣的朋友可以付费学习。
插件化实战课程