Class文件与Dex文件

最近想学下热修复与插件化,第一章就是讲解Class文件和Dex文件的结构与区别,记录下。

以下是整理的思维导图:

Class文件

Class文件是啥

编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件及操作系统的)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式被称为Class文件格式。Class文件格式中精确地定义了类与接口的表示形式。     --Java虚拟机规范
我们知道,高级语言程序被执行需要先进行翻译程序,再进行执行。翻译程序分 编译程序与 解释程序,编译与解释最大的不同是解释程序没有 目标代码的生成。而Java就是一门编译性语言,在执行的过程中会生成中间代码class文件。class文件能够被jvm虚拟机所载入和执行,这样子就能够实现Java“与平台无关”的特性。 Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,比如Python,Scala文件都能生成Class文件。Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。

乱七八糟的说那么多看不懂的接下来进入正题。

文件结构

首先class文件与dex文件都是8字节 二进制文件流,也就是都是由0和1组成,我们可以用 010Editor(强烈安利)这个工具打开。那问题来了,大于8字节的数据项怎么存呢?当需要占用8位字节以上空间的数据项时,则会按照 高位在前的方式分割成若干个8位字节进行存储。(注意区分8字节与8位的区别,1byte=1bit)

在Java虚拟机规范的第四章中有规定class的文件格式

  1. 使用u1,u2和u4(分别代表了1、2和4个字节的无符号数)来表示Class文件的内容

  2. 采用类似c语言结构体的伪结构来描述Class文件格式,把描述类结构格式的内容定义为项,这种伪结构中只有两种数据类型:无符号数和表。在Class文件中,各项按照严格顺序连续存放的,它们之间没有任何填充或对齐作为各项间的分割符号。

  3. 表( Table) 是由任意数量的可变长度的项组成,用于表示 Class 文件内容的一系列复合结构。在Class文件中,表习惯性的使用_info结尾,下文会举详细例子。

Class文件呢 其实是一个大表~

Class文件的生成

要讲Class文件的结构,肯定得打开Class文件研究下,所以讲下class文件的生成。在最开始学Java的时候一般都会使用命令行来编译Java文件,使用的是javac和java命令,而使用 javac命令就能生成class文件。简单的写个HelloWorld.java
public class HelloWorld{
    public static void main(String[] args){
	System.out.println("hello world");
    }
}复制代码
$:javac HelloWorld.java target 1.6 source 1.6
复制代码

-target <release>            生成特定 VM 版本的类文件  

-source <release>            提供与指定发行版的源兼容性

这里我指定target参数是因为之后生成 .dex文件的时候版本太高不支持给报错了。。。

深入Class文件的内部数据结构

用010Editor打开刚刚生成的HelloWorld.class文件:

为了方便查看,010Editor将二进制转换成了十六进制表示,毕竟看一堆01可能会发蒙。上面的十六进制是该Class文件的内容。

看下面那些内容(上面的也看不懂啊(#`O′)。可以看到很多个struct这就是前面提到的伪结构了,最上面的struct ClassFile就是那个“大表"。

每一个 Class 文件对应于一个如下所示的 ClassFile 结构体。

ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}复制代码

来一个个的解释这些项的含义。

magic
魔数,是u4类型, 魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的 Class 文件。 魔数值固定为 0xCAFEBABE(咖啡宝贝~), 不会改变。看上面那张图的开头4字节也验证了这一点。其实不止Class文件中有这项数据,其他文件比如图片文件也是有的,就是可能叫法不同。
minor_version、major_version
副版本号和主版本号, minor_version 和 major_version 的值分别表示 Class 文件的副、 主版本。 它们共同构成了 Class 文件的格式版本号。 譬如某个 Class 文件的主版本号为 M,副版本号为 m,那么这个 Class 文件的格式版本号就确定为 M.m。在我们的HelloWorld.class中,次版本号是0x00,主版本号是0x32,即50.0(16进制的32转换成10进制是50,在010Editor中Vlaue那一项可以查看)。一个 Java 虚拟机实例只能支持特定范围内的主版本号( Mi 至 Mj) 和 0 至特定范围内 ( 0 至 m)的副版本号。不同版本的 Java 虚拟机实现支持的版本号也不同,高版本号的 Java 虚拟机实现可以支持低版本号的Class 文件。
constant_pool_count
常量池常量计数器,一开始看到我还以为是常量池的数量来着~其实是常量的数量。为什么需要有这一项?

表是由可变长数据组成的复合结构( 表中每项的长度不固定),因此无法直接将字节偏移量来作为索引对表进行访问(如果采用这种方式,假设我们需要访问第5个常量,那就需要计算前四个常量的偏移量之和才能访问第5个常量)。 而我们描述一个数据结构为数组时,就意味着它含有零至多个长度固定的项组成,这个时候则可以采用数组索引的方式来访问它。数组需要知道它的长度吧,这个constant_pool_count可以看成是这个数组的长度。我们看前面的图也能发现每个struct cp_info后面都标明了constant_pool[index],在后面我们也有很多类似的结构,都是同样的道理。

当需要描述同一类型但数量不多的数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式。

constant_pool_count 的值等于 constant_pool 表中的成员数加 1。在HelloWorld.class中,constant_pool_count是29,但是常量只有28项。这又是为啥!正常的计数习惯来讲:大小是29,下标应该是0~28。但是在这里计数是从第1项开始的,也就是1~28。把第0项空出来了,这样子可以满足某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。

constant_pool[]
常量池, constant_pool 是一种表结构 ,它包含 Class 文件结构及其子结构中引用的所有字符串常量、 类或接口名、字段名和其它常量。常量池中每一项又是一个表,这些表的结构不相同。但是有一个共同点就是第一项都是tag标志位

下面这个表应该是现在最新的常量池项目类型了,献上官网。value是tag的值

Constant TypeValueclass fileJava SE
CONSTANT_Class745.31.0.2
CONSTANT_Fieldref945.31.0.2
CONSTANT_Methodref1045.31.0.2
CONSTANT_InterfaceMethodref1145.31.0.2
CONSTANT_String845.31.0.2
CONSTANT_Integer345.31.0.2
CONSTANT_Float445.31.0.2
CONSTANT_Long545.31.0.2
CONSTANT_Double645.31.0.2
CONSTANT_NameAndType1245.31.0.2
CONSTANT_Utf8145.31.0.2
CONSTANT_MethodHandle1551.07
CONSTANT_MethodType1651.07
CONSTANT_InvokeDynamic1851.07
CONSTANT_Module1953.09
CONSTANT_Package2053.09
不到黄河心不死,来验证下表中内容。下图可以看到第一个常量的tag值是10,是 CONSTANT_Methodref类型,正好跟上面的表对应上了~而class_index是指向常量池中一个CONSTANT_Class类型的常量,表示声明该方法的 描述符的索引项。这里值是6,即第6项。由于010Editor对常量池是从0开始编号,所以我们应该找下标为5的常量,可以看到是CONSTANT_Class类型。

而CONSTANT_Class类型中的name_index是指向一个CONSTANT_Utf8类型的常量,表示该类的全限定名。

可以如下使用命令查看class文件的内容

$ javap -verbose HelloWorld 
  下图就是运行该命令后的内容,这里主版本是53的原因是我又编译了一次这个Java类但是忘了指定target参数造成的复制代码
就叫这张图是图6吧~
access_flags
访问标志, access_flags 是一种掩码标志, 用于表示某个类或者接口的访问权限及基础属性。取值范围和意义如下:
Class access and property modifiers
Flag NameValueInterpretation
ACC_PUBLIC0x0001Declared public; may be accessed from outside its package.
ACC_FINAL0x0010Declared final; no subclasses allowed.
ACC_SUPER0x0020Treat superclass methods specially when invoked by the invokespecial instruction.此处“特殊处理”是相对于 JDK 1.0.2 之前的 Class 文件而言, invokespecial 的语义和处理方式在 JDK 1.0.2 时发生了变化,为避免二义性,在 JDK 1.0.2 之后编译出的 Class 文件,都带有 ACC_SUPER标志用以区分
ACC_INTERFACE0x0200Is an interface, not a class.
ACC_ABSTRACT0x0400Declared abstract; must not be instantiated(实例化).
ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.
ACC_ANNOTATION0x2000Declared as an annotation type.
ACC_ENUM0x4000Declared as an enum type.
ACC_MODULE0x8000Is a module, not a class or interface.
在上表中没有使用的 access_flags 标志位是为未来扩充而预留的,这些预留的标志为在编译器中会被设置为 0, Java 虚拟机实现也会自动忽略它们。

比如ACC_MODULE是Java SE9加进来的~

this_class、super_class
类索引, this_class 的值必须是对 constant_pool 表中项目的一个有效索引值。constant_pool 表在这个索引处的项必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接口。父类索引,对于类来说, super_class 的值必须为 0 或者是对 constant_pool 表中项目的一个有效索引值。 比如图6中红色圈出来的:this_class 指向#5,#5的值是HelloWorld;super_class指向#6,#6的值是 java/lang/Object。
 interfaces_count
接口计数器, interfaces_count 的值表示当前类或接口的 直接父接口数量(对于类是实现,对于接口是继承)。
interfaces[]
接口表, interfaces[]数组中的每个成员的值必须是一个对 constant_pool 表中项目的一个有效索引值, 它的长度为 interfaces_count。如果没有实现接口,这一项就没有~
fields_count
字段计数器, fields_count 的值表示当前 Class 文件 fields[]数组的成员个数。fields[]中每一项都是一个 field_info 结构的数据项, 它用于表示该类或接口声明的类字段或者实例字段(成员变量和实例变量)。
fields[]
字段表, fields[]数组描述当前类或接口声明的所有字段,但不包括从父类或父接口继承的部分。
methods_count
u2类型,方法计数器, methods_count 的值表示当前 Class 文件 methods[]数组的成员个数,即方法的个数,所以一个类中如果方法个数超过了65535,会编译失败。(难道这就是传说中Android的65535问题?可能有点原因...
methods[]
方法表,methods[]中每一项都是一个 method_info 结构的数据项,用于表示当前类或接口中某个方法的完整描述(包括方法名,方法签名,方法修饰符,方法中的代码)。methods[]数组只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。

access_flags描述的是方法的修饰符;name_index描述的是方法名的简单名称;

description_index是方法的描述符。描述符的作用是用来描述字段的数据类型、方法的参数列表和返回值。比如method[1] : public static void main(String[] args) 描述符是 ([Ljava/lang/String;) V  。()表示是个方法,参数是String[] , 返回值类型是void。

attributes_count:attribute_info表的属性数量,具体下面会再提到~

方法表可以对照着下面两张图看(注意只有常量池是从1开始计数,包括前面的字段和接口表也都是0开始计数):

attributes_count、attributes[](最后两个惹!)
属性计数器、属性表。这一段好长。。我感觉这一块是最复杂的结构了属性表里面有属性表里面还有属性表。//待填坑

本文中一些内容是参考《深入理解jvm虚拟机》和《Java虚拟机规范》中的,如有雷同,那就是我复制粘贴的。

转载于:https://juejin.im/post/5ad55c596fb9a028c71ef0b6

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值