本文目标
深入理解Android
的类加载机制
- 1.什么是双亲委派
- 2.双亲委派下的
Class
文件加载流程 - 3.
Android
中的类加载器 - 4.
PathClassLoader
和DexClassLoader
到底有何不同 - 5.
Class
文件加载步骤和源码分析 - 6.
Class.forName
和ClassLoader.loadClass
加载有何不同
1.什么是双亲委派
- 1.加载
.class
文件的时候,以递归的的形式逐级向上委托给父加载器ParentClassLoader
去加载,如果加载过了,就不用在加载一遍 - 2.如果父加载器也没加载过,则继续委托给父加载器去加载,一直到这条链路的顶级,顶级
classLoader
判断如果没加载过,则尝试加载,加载失败,则逐级向下交还调用者来加载.
双亲委派是如何实现的
任意一个classLoader
对象都会有一个parent
对象,我们下面的customClassLoader
创建的时候虽然没有传递parent
对象,但是在下面的ClassLoader
类中的空参构造方法可以看出,会调用getSystemClassLoader()
从而调用ClassLoader.createSystemClassLoader();
,最后创建了一个PathClassLoader
对象作为parent
,而且在创建PathClassLoader
的同时也指定了它的parent
为BootClassLoader
ClassLoader customClassLoader= new ClassLoader() {
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
};
public abstract class ClassLoader {
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
//最终会调用PathClassLoader这个classLoader
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//1.先检查是否已经加载过--findLoaded
Class> c = findLoadedClass(name);
if (c == null) {
try {
//2.如果自己没加载过,存在父类,则委托父类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
//3.如果父类也没加载过,则尝试本级classLoader加载
c = findClass(name);
}
}
return c;
}
}
来继续看是如何实现的,看下loadClass
方法,对于任意一个classLoader
对象来说,来加载文件的时候都会调用loadClass
方法
- 1.先检查自己是否已经加载过
class文件了
--用这个findLoadedClass
方法,如果已经加载了直接返回就好了 - 2.如果自己没加载过,存在父类,则委托父类去加载--用这个
parent.loadClass(name, false)
方法,此时就会向上传递,然后就会去父加载器中循环第1步,一直到顶级ClassLoader
- 3.如果父类也没加载过,则尝试本级
classLoader
加载,如果加载失败了就会向下传递,交给调用方来实现.class
文件的加载
2.双亲委派下的Class
文件加载流程
总结一下,比如说现在要去加载个String.class
文件,我们自己定义了一个CustomerClassLoader
- 首先会判断自己的
CustomerClassLoader
否加载过,如果加载过直接返回, - 如果没有加载过则会调用父类
PathClassLoader
去加载,该父类同样会判断自己是否加载过,如果没有加载过则委托给父类BootClassLoader
去加载, - 这个
BootClassLoader
是顶级classLoader
,同样会去判断自己有没有加载过,如果也没有加载过则会调用自己的findClass(name)
去加载, - 如果顶级
BootClassLoader
加载失败了,则会把加载这个动作向下交还给PathClassLoader
, - 这个
PathClassLoader
也会尝试去调用findClass(name);
去加载,如果加载失败了,则会继续向下交还给CustomClassLoader
来完成加载
这整个过程感觉是一个递归的过程,逐渐往上然后有逐渐往下,直到加载成功
其实这个String.class
在系统启动的时候已经被加载了,我们自己定义一个CustomerClassLoader
去加载,其实也是父类加载的
双亲委派的作用
- 1.防止同一个
.class
文件重复加载 - 2.对于任意一个类确保在虚拟机中的唯一性.由加载它的类加载器和这个类的全类名一同确立其在Java虚拟机中的唯一性
- 3.保证
.class
文件不被篡改,通过委托方式可以保证系统类的加载逻辑不被篡改.
3.Android
中的主要类加载器
- 1.
PathClassLoader
复杂的加载系统类和英勇程序的类,通常用来加载已安装apk
的dex
文件,实际上外部存储的dex
文件也能加载 - 2.
DexClassLoader
可以加载dex
文件以及包含dex
的压缩文件(apk,dex,jar,zip) - 3.
BaseDexClassLoader
实际应用层类文件的加载,而真正的加载逻辑委托给pathList
来完成 - 4.
BootClassLoader
Android平台上所有ClassLoader
的最终parent
,Android系统启动时会使用BootClassLoader
来预加载常用类
4.PathClassLoader
和DexClassLoader
到底有何不同以及源码解析
我们可以发现
PathClassLoader
和DexClassLoader
都继承自BaseDexClassLoader
,然后BaseDexClassLoader
继承自ClassLoader
,
具体源码如下
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
public class DexClassLoader extends BaseDexClassLoader {
//dexpath:dex文件以及包含dex的apk文件或jar文件的路径,多个路径用文件分隔符分割,默认文件分隔符为 ': '
//optimizedDirectory:Android系统将dex文件进行优化后所生产的ODEX文件的存放路径,该路径必须是一个
//PathClassLoader中默认使用路径"/data/dalvik-cache"
//而DexClassLoader则需要我们指定ODEX文件的存放路径
//librarySearchPath:所使用到的C/C++库存放的路径
//parent:这个参数的主要作用是保留java中ClassLoader的委托机制
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
阅读源码,我们可以发现PathClassLoader
和DexClassLoader
都没有重写findClass(name);
方法,仅仅只是暴露出来1-2个构造方法而已,他俩啥事也没干,在Android 8.0
以后两个类功能是一样的
参数解析
- 1.
dexpath
:dex
文件以及包含dex
的apk
文件或jar文件
的路径,多个路径用文件分隔符分割,默认文件分隔符为': '
- 2.
optimizedDirectory
:Android
系统将dex文件
进行优化后所生产的ODEX文件
的存放路径,该路径必须是一个内部存储路径
PathClassLoader
中默认使用路径"/data/dalvik-cache"
而DexClassLoader
则需要我们指定ODEX文件
的存放路径 - 3.
librarySearchPath
:所使用到的C/C++
库存放的路径 - 4.
parent
:这个参数的主要作用是保留java中ClassLoader的委托机制
这个PathClassLoader
主要是用来加载APK
中的文件的,然后有没有重新findClass(name);
来实现文件加载,我们就去父类BaseDexClassLoader
中找该方法,
public class BaseDexClassLoader extends ClassLoader {
public BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath,
ClassLoader parent,boolean isTrusted) {
super(parent);
......
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
......
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
//资源文件的加载
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
//class文件的加载
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
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(String name)
和findResource(String name)
和findLibrary(String name)
等方法都是通过pathList
这个对象来进行加载的,然后这个pathList
对象是在构造函数中创建的,来具体看下
final class DexPathList {
/** list of dex/resource (class path) elements */
private final Element[] dexElements;
/** list of native library directory elements */
private final File[] nativeLibraryDirectories;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory,boolean isTrusted) {
......
//1.根据传进来的dexPath路径加载出来所有的dex文件
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
//2.根据libraryPath加载出来所有的动态库文件
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
/**
* Element of the dex/resource file path
*/
/*package*/ static class Element {
public final File file;
public final ZipFile zipFile;
public final DexFile dexFile;
public Element(File file, ZipFile zipFile, DexFile dexFile) {
this.file = file;
this.zipFile = zipFile;
this.dexFile = dexFile;
}
public URL findResource(String name) {
if ((zipFile == null) || (zipFile.getEntry(name) == null)) {
return null;
}
try {
return new URL("jar:" + file.toURL() + "!/" + name);
} catch (MalformedURLException ex) {
throw new RuntimeException(ex);
}
}
}
}
该DexPathList
创建的时候 主要干了
- 1.根据传进来的dexPath路径加载出来所有的
dex
文件,重要参数dexElements
- 2.根据libraryPath加载出来所有的动态库文件
来我们继续看dexElements
是如何被加载进来的,具体看下makeDexElements(splitDexPath(dexPath), optimizedDirectory);
这个方法
首先会先调用splitDexPath(dexPath)
对传进来的dexPath
做一个分割,然后会返回一个File的集合
final class DexPathList {
private static ArrayList splitDexPath(String path) {
return splitPaths(path, false);
}
private static List splitPaths(String searchPath, boolean wantDirectories) {
List result = new ArrayList<>();
if(searchPath!=null){
for(String path : searchPath.split(File.pathSeparator)){
if(directoriesOnly){
try{
StructStat sb = Libcore.os.stat(path);
if(!S_ISDIR(sb.st_mode)){
continue;
}
}catch(ErrnoException ignored){
continue;
}
}
result.add(new File(path));
}
}
}
}
接下来看makeDexElements
方法
private static Element[] makeDexElements(List files, File optimizedDirectory,
List suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* 遍历传进来的File集合
*/
for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
//判断是否是已 .dex结尾的
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} else {//不是以.dex结尾的,有可能是zip或者jar文件,照样跟上面的逻辑一样
dex = loadDexFile(file, optimizedDirectory, loader, elements);
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
//android 8.0以后 optimizedDirectory 是废弃掉的
if (optimizedDirectory == null) {
return new DexFile(file,loader,elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0,loader,elements);
}
}
该方法内部主要是一个for循环
把应用中的所有dex文件
遍历了出来,被加载出来之后都会存储到dexElements
这个数组中,这个dex文件
可以理解为一个一个的文件夹,这个文件夹中含有很多class
文件
app可以把启动的
.class
文件集中打进第一个dex
包中,这是启动优化的一个思路
然后我们这个第一个
dex
文件夹和下面的classes2.dex
中都包含一个MainActivity.class
文件,则会优先加载第一个,这也就是热修复的鼻祖方案
5.1Class
文件加载步骤和源码分析
类的加载指的是将类的.class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class
对象,用来封装类在方法区内的数据结构,并且提供了访问方法区内的数据结构的方法.
- 1.通过
Class.forName()
方法动态加载 - 2.通过
ClassLoader.loadClass()
方法动态加载
类的加载分为3个步骤:1.装载(Load)
,2.链接(Link)
,3.初始化(Intialize)
1.装载(Load)查找并加载类的二进制数据(查找和导入Class文件)
- 通过一个类的全限定名来获取其定义的二进制字节流
- 将这个字节流转化为方法区的运行时数据结构
- 在
Java堆
中生成一个代表这个类的java.lang.Class
对象,作为方法区中这些数据的访问入口
2.链接(Link)
验证:确保被加载的类的正确性
- 文件格式验证:验证字节流是否符合
Class文件
格式的规范;比如说,是否以0xCAFEBABE
开头,主次版本是不在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型 - 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合
Java语言规范的要求
,比如说这个类是否有父类,除了java.lang.Object
之外 - 字节码验证:通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的
准备:为类的静态变量分配内存,并将其初始化为默认值
- 这时候进行内存分配的仅包括
类变量(static)
,而不包括实例变量,实例变量
会在对象实例化
时候随着对象一块分配在java堆
中 - 这里所设置的初始值通常情况下是数据类型默认的零值,
(如0,0L,null,false)
,而不是被在java代码中被显示地赋予的值
解析:把类中的符号引用转换为直接引用
- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类,接口,字段,类方法,接口方法,方法类型.符号引用就是一组符号来描述目标,可以是任何字面量.
直接引用就是直接指向目标的内存地址指针
3.初始化(Intialize):指向类的方法,对类的静态变量,静态代码块执行初始化操作(不是必须的)
这个初始化不是执行构造函数,也不是执行字节码中的init
方法,是执行的类构造器的cinit
方法,
-
cinit
方法是在这个class
在初始化的时候才会执行的 -
init
方法是在这个class
所对应的java对象被实例化的时候才会执行
以下方式会触发类的初始化
- 1.创建类的实例,也就是new一个对象
- 2.访问某个类或接口的静态变量,或者对该静态变量赋值
- 3.调用类的静态方法
- 4.反射
Class.forName("android.app.ActivityThread")
- 5.初始化一个类的子类(会首先初始化子类的父类)
- 6.JVM启动时标明的启动类,即文件名和类名相同的那个类
5.2Class
文件字节码分析
先抛一个问题,为什么静态方法不能访问非静态变量?,我们来通过一个简单的class类看一下字节码就能明白了
public class Test {
public static int num = 3;
public int num2 = 3;
static void test(){
System.out.println("test");
}
}
.class public Lcom/yadong/day01_lsn01/Test;
.super Ljava/lang/Object;
.source "Test.java"
# static fields 声明的是静态的
.field public static num:I
# instance fields 声明的是非静态的
.field public num2:I
# direct methods
.method static constructor ()V
.registers 1
.line 12
const/4 v0, 0x3
# 静态的是在这里调用的 num,为num进行赋值
sput v0, Lcom/yadong/day01_lsn01/Test;->num:I
return-void
.end method
.method public constructor ()V
.registers 2
.line 9
invoke-direct {p0}, Ljava/lang/Object;->()V
.line 15
const/4 v0, 0x3
# 非静态的是在这里调用的 num2,为num2进行赋值
iput v0, p0, Lcom/yadong/day01_lsn01/Test;->num2:I
return-void
.end method
.method static test()V
.registers 2
.line 19
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "test"
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
.line 20
return-void
.end method
我们能看出来,静态字段num
和非静态字段num1
是在不同的地方初始化的
- 静态字段
num
是在类构造器clinit
方法内初始化并赋值的 - 非静态字段
num1
是在init
方法内初始化并赋值的,只有java实例化对象实例的时候才会执行init
方法
为什么静态方法不能访问非静态变量?
public class MainActivity {
//在准备阶段他的值默认为0,初始化阶段才会被赋值为3
//因为把value赋值为3的public static语句在编译后的指令是在类构造器()方法之中被调用的
static int value =3;
int value2 = 3;//随着对象实例化的时候,才会被赋值
static void test(){
value2=100;//静态方法为什么不能访问非静态变量
}
}
总结: 当我们在调用MainActivity.test()
这个静态方法的时候,这时候并没创建MainActivity
的实例对象,既然没有创建实例对象,那字段value2
会被赋值么?肯定不会,那肯定不会访问
如何编译字节码步骤如下
6.1Class.forName
和 ClassLoader.loadClass
加载有何不同
-
Class.forName
除了将类的.class
文件加载到jvm中之外,还会对类进行解释,执行类中的static
静态代码块.注意这里的静态块指的是在类初始化时的一些数据,但是classLoader
却没有. -
Class.forName()
方法实际上也是调用的CLassLoader
来实现的
总结一下就是:
虽然ClassLoader.loadClass
也能加载一个类,但是不会触发类的初始化(也就是说不会对类的静态变量,静态代码块进行初始化操作),
Class.forName
这种方式,不但会加载一个类,还会触发类的初始化阶段,也能够为这个类的静态变量,静态代码块进行初始化操作
6.2源码分析
先分析下Class.forName
这种方式
public final class Class {
public static Class> forName(String className)throws ClassNotFoundException {
//1.如果是在MainActivity中调用的Class.forName,则这个caller就是MainActivity.class对象
Class> caller = Reflection.getCallerClass();
// 2.ClassLoader.getClassLoader(caller) 得到的是当前这个类(MainActivity.class)的ClassLoader对象
return forName(className, true, ClassLoader.getClassLoader(caller));
}
/**
*initialize为true:类被加载的时候会触发类的初始化操作
*/
@CallerSensitive
public static Class> forName(String name, boolean initialize,ClassLoader loader) throws ClassNotFoundException {
if (loader == null) {
loader = BootClassLoader.getInstance();
}
Class> result;
try {
//调用这个native层的方法得到result
result = classForName(name, initialize, loader);
} catch (ClassNotFoundException e) {
Throwable cause = e.getCause();
if (cause instanceof LinkageError) {
throw (LinkageError) cause;
}
throw e;
}
return result;
}
@FastNative
static native Class> classForName(String className, boolean shouldInitialize,
ClassLoader classLoader) throws ClassNotFoundException;
public ClassLoader getClassLoader() {
if (isPrimitive()) {
return null;
}
return (classLoader == null) ? BootClassLoader.getInstance() : classLoader;
}
}
public abstract class ClassLoader {
// Returns the class's class loader, or null if none.
static ClassLoader getClassLoader(Class> caller) {
if (caller == null) {
return null;
}
return caller.getClassLoader();
}
}
先看第一个方法public static Class> forName(String className)throws ClassNotFoundException
- 1.如果是在MainActivity中调用的Class.forName,则这个caller就是MainActivity.class对象
- 2.ClassLoader.getClassLoader(caller) 得到的是当前这个类(MainActivity.class)的ClassLoader对象
- 3.然后就调用了
forName(className, true, ClassLoader.getClassLoader(caller));
继续追进去发现,调用这个native层
的方法得到result
在native层的java_lang_Class.cp
文件中,这行代码就是类的初始化
来看下ClassLoader.loadClass
这种方式是如何实现的
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
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;
}
}
final class DexPathList {
public Class> findClass(String name, List suppressed) {
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;
}
static class Element {
public Class> findClass(String name, ClassLoader definingContext,List suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed): null;
}
}
}
public final class DexFile {
public Class loadClassBinaryName(String name, ClassLoader loader, List suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List 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;
}
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,DexFile dexFile)
throws ClassNotFoundException, NoClassDefFoundError;
}
最后会走到这里,发现并没有做任何初始化类的操作
至此,全部分析完毕