关于做者html
郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工做,欢迎交流技术方面的问题,能够去个人Github提issue或者发邮件至guoxiaoxingse@163.com与我交流。java
文章目录android
这篇文章咱们来聊一聊关于Android虚拟机的那些事,固然这里咱们并不须要去讲解关于虚拟机的底层细节,所讲的东西都是你们日常在开发中常常用的。例如类的加载机制、资源加载机制、APK打包流程、APK安装流程 以及Apk启动流程等。讲解这些知识是为了后续的文章《大型Android项目的工程化实践:插件化》、《大型Android项目的工程化实践:热更新》、《大型Android项目的工程化实践:模块化》等系列的文章作一个 原理铺垫。git
好了,让咱们开始吧~😁程序员
Class文件是一组以8位字节为基础的单位的二进制流,各个数据项按严格的顺序紧密的排列在Class文件中,中间没有任何间隔。github
这么说有点抽象,咱们先来举一个简单的小例子。🤞数组
public class TestClass {
public int sum(int a, int b) {
return a + b;
}
}
复制代码
编译生成Class文件,而后使用hexdump命令查看Class文件里的内容。缓存
javac TestClass.java
hexdump TestClass.class
复制代码
Class文件内容以下所示:bash
Classfile /Users/guoxiaoxing/Github-app/android-open-source-project-analysis/demo/src/main/java/com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass.class
Last modified 2018-1-23; size 333 bytes
MD5 checksum 72ae3ff578aa0f97b9351522005ec274
Compiled from "TestClass.java"
public class com.guoxiaoxing.android.framework.demo.native_framwork.vm.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass.m:I
#3 = Class #17 // com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass
#18 = Utf8 java/lang/Object
{
public com.guoxiaoxing.android.framework.demo.native_framwork.vm.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 15: 0
}
SourceFile: "TestClass.java"
复制代码
Class文件十六机制内容以下所示:cookie
注:笔者用的二进制查看软件是iHex,能够去AppStore下载,Windows用户可使用WinHex。
这是一份十六进制表示的二进制流,每一个位排列紧密,都有其对应的含义,具体说来,以下所示:
注:下列表中四个段分别为 类型、名称、说明、数量
咱们能够看着在上面这张表中有相似u二、attribute_info这样的类型,事实上Class文件采用一种相似于C语言结构体的伪结构struct来存储数据,这种结构有两种数据类型:
咱们分别来看看上述的各个字段的具体含义已经对应数值。
注:这一块的内容可能有点枯燥,可是它是咱们后续学习类加载机制,Android打包机制,以及学习插件化、热更新框架的基础,因此须要掌握。 可是也不必都记住每一个段的含义,你只须要有个总体性的认识便可,后续若是忘了具体的内容,能够再回来查阅。😁
具体含义
魔数:1-4字节,用来肯定这个文件是否为一个能被虚拟机接受的Class文件,它的值为0xCAFEBABE。
对应数值
ca fe ba be
具体含义
版本号:5-6字节是次版本号,7-8字节是主版本号
对应数值
5-6字节是次版本号0x0000(即0),7-8字节是主版本号0x0034(即52).
JDK版本号与数值的对应关系以下所示:
具体含义
常量池计数:常量池中常量的数量不是固定的,所以常量池入口处会放置一项u2类型的数据,表明常量池容器计数。注意容器计数从1开始,索引为0表明不引用任何一个 常量池的项目。
对应数值
9-10字节是常量池容器计数0x0013(即19)。说明常量池里有18个常量,从1-18.
这是咱们上面用javap分析的字节码文件里的常量池里常量的个数是一直的。
举个常量池里的常量的例子🤞
它的常量值以下所示:
#17 = Utf8 com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass
复制代码
常量池主要存放字面量与符号引用。
字面量包括:
符号引用包括:
常量池里的每一个常量都用一个表来表示,表的结构以下所示:
cp_info {
//表明常量类型
u1 tag;
//表明存储的常量,不一样的常量类型有不一样的结构
u1 info[];
}
复制代码
目标一共有十四中常量类型,以下所示:
注:下表字段分别为 类型、标志(tag)、描述
具体含义
访问标志:常量池以后就是访问标志,该标志用于识别一些类或则接口层次的访问信息。这些访问信息包括这个Class是类仍是接口,是否认义Abstract类型等。
对应数值
常量池以后就是访问标志,前两个字节表明访问标志。
从上面的分析中常量池最后一个常量是#14 = Utf8 java/lang/Object,因此它后面的两个字节就表明访问标志,以下所示:
访问表示值与含义以下所示:
咱们上面写了一个普通的Java类,ACC_PUBLIC位为真,又因为JDK 1.0.2之后编译出来的类ACC_SUPER标志位都为真,因此最终的值为:
0x0001 & 0x0020 = 0x0021
复制代码
这个值就是上图中的值。
具体含义
类索引(用来肯定该类的全限定名)、父类索引(用来肯定该类的父类的全限定名)是一个u2类型的数据(单个类、单继承),接口索引是一个u2类型的集合(多接口实现,用来描述该类实现了哪些接口)
对应数值
类索引、父类索引与接口索引牢牢排列在访问标志以后。
类索引为0x0002,它的全限定名为com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass。
父类索引为0x0003,它的全限定名为java/lang/Object。
接口索引的第一项是一个u2类型的数据表示接口计数器,表示实现接口的个数。这里没有实现任何接口,因此为0x0000。
具体含义
字段表用来描述接口或者类里声明的变量、字段。包括类级变量以及实例级变量,但不包括方法内部声明的变量。
字段表结构以下所示:
field_info {
u2 access_flags;//访问标志位,例如private、public等
u2 name_index;//字段的简单名称,例如int、long等
u2 descriptor_index;//方法的描述符,描述字段的数据类型,方法的参数列表和返回值
u2 attributes_count;
attribute_info attributes[attributes_count];
}
复制代码
access_flags取值以下所示:
descriptor_index里描述符的含义以下所示:
对应数值
方法便用来描述方法相关信息。
方法表的类型与字段表彻底相同,以下所示:
method_info {
u2 access_flags;//访问标志位,例如private、public等
u2 name_index;//方法名
u2 descriptor_index;//方法的描述符,描述字段的数据类型,方法的参数列表和返回值
u2 attributes_count;
attribute_info attributes[attributes_count];
}
复制代码
对应的值
后续还有属性表集合等相关信息,这里就再也不赘述,更多内容请参见Java虚拟机规范(Java SE 7).pdf。
经过上面的描述,咱们理解了Class存储格式的细节,那么这些是如何被加载到虚拟机中去的呢,加载到虚拟机以后又会发生什么变化呢?🤔
咱们接着来看。
什么是类的加载?🤔
类的加载就是虚拟机经过一个类的全限定名来获取描述此类的二进制字节流。
类加载的流程图以下所示:
加载
事实上,从哪里将一个类加载成二进制流是有很开发的,具体说来:
验证
验证主要是验证加载进来的字节码二进制流是否符合虚拟机规范。
准备
准备阶段正式为类变量分为内存并设置变量的初始值,所使用的内存在方法去里被分配,这些变量指的是被static修饰的变量,而不包括实例的变量,实例的变量会伴随着对象的实例化一块儿在Java堆 中分配。
解析
解析阶段将符号引用转换为直接引用,符号引用咱们前面已经说过,它以CONSTANT_class_info等符号来描述引用的目标,而直接引用指的是这些符号引用加载到虚拟机中之后 的内存地址。
这里的解析主要是针对咱们上面提到的字段表、方法表、属性表里面的信息,具体说来,包括如下类型:
初始化
初始化阶段开始执行类构造器()方法,该方法是由全部类变量的赋值动做和static语句块合并产生的
关于类构造器()方法,它和实例构造器()是不一样的,关于这个方法咱们须要注意如下几点:
讲完了类的加载流程,咱们接着来看看类加载器。
类的加载就是虚拟机经过一个类的全限定名来获取描述此类的二进制字节流,而完成这个加载动做的就是类加载器。
类和类加载器息息相关,断定两个类是否相等,只有在这两个类被同一个类加载器加载的状况下才有意义,不然即使是两个类来自同一个Class文件,被不一样类加载器加载,它们也是不相等的。
注:这里的相等性保函Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果以及Instance关键字对对象所属关系的断定结果等。
类加载器能够分为三类:
这么多类加载器,那么当类在加载的时候会使用哪一个加载器呢?🤔
这个时候就要提到类加载器的双亲委派模型,流程图以下所示:
双亲委派模型的整个工做流程很是的简单,以下所示:
若是一个类加载器收到了加载类的请求,它不会本身当即去加载类,它会先去请求父类加载器,每一个层次的类加载器都是如此。层层传递,直到传递到最高层的类加载器,只有当 父类加载器反馈本身没法加载这个类,才会有当前子类加载器去加载该类。
关于双亲委派机制,在ClassLoader源码里也能够看出,以下所示:
public abstract class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//首先,检查该类是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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) {
//若是父类加载器没有加载到该类,则本身去执行加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
}
复制代码
为何要这么作呢?🤔
这是为了要让越基础的类由越高层的类加载器加载,例如Object类,不管哪一个类加载器去尝试加载这个类,最终都会传递给最高层的类加载器去加载,前面咱们也说过,类的相等性是由 类与其类加载器共同断定的,这样Object类不管在何种类加载器环境下都是同一个类。
相反若是没有双亲委派模型,那么每一个类加载器都会去加载Object,那么系统中就会出现多个不一样的Object类了,如此一来系统的最基础的行为也就没法保证了。
理解了JVM上的类加载机制,咱们再来看看Android虚拟机上上是如何加载类的。
Java虚拟机加载的是class文件,而Android虚拟机加载的是dex文件(多个class文件合并而成),因此二者既有类似的地方,也有所不一样。
Android类加载器类图以下所示:
能够看到Android类加载器的基类是BaseDexClassLoader,它有派生出两个子类加载器:
除了这两个子类觉得,还有两个类:
咱们先来看看基类BaseDexClassLoader的构造方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
复制代码
BaseDexClassLoader构造方法的四个参数的含义以下:
DexClassLoader与PathClassLoader都继承于BaseDexClassLoader,这两个类只是提供了本身的构造函数,没有额外的实现,咱们对比下它们的构造函数的区别。
PathClassLoader
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);
}
}
复制代码
DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
复制代码
能够发现这两个类的构造函数最大的差异就是DexClassLoader提供了optimizedDirectory,而PathClassLoader则没有,optimizedDirectory正是用来存放odex文件 的地方,之后能够利用DexClassLoader实现动态加载。
上面咱们也说过,Dex的加载以及Class额查找都是由DexFile调用它的native方法完成的,咱们来看看它的实现。
咱们来看看Dex文件加载、类的查找加载的序列图,以下所示:
从上图Dex加载的流程能够看出,optimizedDirectory决定了调用哪个DexFile的构造函数。
若是optimizedDirectory为空,这个时候实际上是PathClassLoader,则调用:
DexFile(File file, ClassLoader loader, DexPathList.Element[] elements)
throws IOException {
this(file.getPath(), loader, elements);
}
复制代码
若是optimizedDirectory不为空,这个时候实际上是DexClassLoader,则调用:
private DexFile(String sourceName, String outputName, int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}
mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
mFileName = sourceName;
//System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}
复制代码
因此你能够看到DexClassLoader在加载Dex文件的时候比PathClassLoader多了一个openDexFile()方法,该方法调用的是native方法openDexFileNative()方法。
这个方法并非真的打开Dex文件,而是将Dex文件以一种mmap的方式映射到虚拟机进程的地址空间中去,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,虚拟机 进程就能够采用指针的方式读写操做这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操做而没必要再调用read,write等系统调用函数。
关于mmap,它是一种颇有用的文件读写方式,限于篇幅这里再也不展开,更多关于mmap的内容能够参见文章:http://www.cnblogs.com/huxiao-tee/p/4660352.html
到这里,Android虚拟机的类加载机制就讲的差很少了,咱们再来总结一下。
Android虚拟机有两个类加载器DexClassLoader与PathClassLoader,它们都继承于BaseDexClassLoader,它们内部都维护了一个DexPathList的对象,DexPathList主要用来存放指明包含dex文件、native库和优化odex目录。 Dex文件采用DexFile这个类来描述,Dex的加载以及类的查找都是经过DexFile调用它的native方法来完成的。