深入理解Android:Java虚拟机ART
上QQ阅读APP看书,第一时间看更新

3.1.1 Dex和Class文件格式的区别

Dex文件和Class文件的区别有很多,本文先来看如下几点区别。

3.1.1.1 字节码文件的创建

一个Class文件对应一个Java源码文件,而一个Dex文件可对应多个Java源码文件。开发者开发一个Java模块(不管是Jar包还是Apk)时:

·在PC平台上,该模块包含的每一个Java源码文件都会对应生成一个同文件名(不包含后缀)的.class文件。这些文件最终打包到一个压缩包(即Jar包)中。

·而在Android平台上,这些Java源码文件的内容最终会编译、合并到一个名为classes.dex的文件中。不过,从编译过程来看,Java源文件其实会先编译成多个.class文件,然后再由相关工具将它们合并到Jar包或Apk包中的classes.dex文件中。

读者可以推测一下Dex文件的这种做法有什么好处。笔者至少能想出如下两个优点:

·虽然Class文件通过索引方式能减少字符串等信息的冗余度,但是多个Class文件之间可能还是有重复字符串等信息。而classes.dex由于包含了多个Class文件的内容,所以可以进一步去除其中的重复信息。

·如果一个Class文件依赖另外一个Class文件,则虚拟机在处理的时候需要读取另外一个Class文件的内容,这可能会导致CPU和存储设备进行更多的I/O操作。而classes.dex由于一个文件就包含了所有的信息,相对而言会减少I/O操作的次数。

3.1.1.2 字节序

Java平台上,字节序采用的是Big Endian。所以,Class文件的内容也采用Big Endian字节序来组织其内容。而Android平台上的Dex文件默认的字节序是Little Endian(这可能是因为ARM CPU(也包括X86 CPU)采用的也是Little endian字节序的原因吧)。那么,这两种字节序有什么区别呢?来看一个示例,如图3-1所示为一个内容只有4个字节长度的文件。

图3-1 Big Endian和Little Endian的区别

结合图3-1,我们以如何解析从文件中读到的4个字节的内容为例来解释两种字节序的区别。

·首先,文件的内容按从左至右,由低到高排布,第一个字节的内容是0x01,第二个字节的内容是0x02,第三个字节的内容是0x03,第四个字节的内容是0x04。

·字节序只涉及字节和字节之间的顺序,不涉及字节内部各比特位的高低顺序。

·假设外界把这四个字节当作int型来处理,当以Big Endian格式来处理它们时,由于Big Endian是高地址存储低字节内容,低地址存储高字节内容,所以这个整数的值是(0x01<<24)|(0x02<<16)|(0x03<<8)|(0x04<<0)。

·当以Little Endian来处理这四个字节的时候,由于Little Endian是高地址存储高字节内容,低地址存储低字节内容,则这个整数的值是(0x04<<24)|(0x03<<16)|(0x02<<8)|(0x01<<0)。

字节序貌似处理起来麻烦,不过Java ByteBuffer类提供了一个非常简单API,它可以很方便处理不同字节序的问题。下面是笔者针对上述示例写的一段代码。

[testEndian代码]

public static void testEndian(){
    byte[] content = new byte[]{0x01,0x02,0x03,0x04};//内容
    //按LittleEndian方式解析得到的期望值
    int littleEndianExpectedValue =
         (0x04<<24)|(0x03<<16)|(0x02<<8)|(0x01<<0);
    //按BigEndian方式解析得到的期望值
    int bigEndianExpectedValue =
         (0x01<<24)|(0x02<<16)|(0x03<<8)|(0x04<<0);
    //创建一个ByteBuffer(java.nio包中),并设置字节序为BigEndian
    ByteBuffer byteBuffer = ByteBuffer.wrap(content);
    byteBuffer.order(ByteOrder.BIG_ENDIAN);
    int readValue = byteBuffer.getInt();
    //比较readValue和bigEndianExpectedValue
    assert(readValue==bigEndianExpectedValue);
    //ByteBuffer回滚到第一个字节以重新读取其内容。
    byteBuffer.rewind();
    //这次设置字节序为Little Endian,
    byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
    readValue = byteBuffer.getInt();
    //比较readValue和littleEndianExpectedValue
    assert(readValue==bigEndianExpectedValue);
}

3.1.1.3 新增LEB128数据类型

为了进一步减少文件空间,Dex文件定义了一种名为LEB128的数据类型。LEB128是Little Endian Based 128的缩写,其唯一功能就是用于表示32比特位长度的数据。它的好处是什么呢?

我们知道传统的int型数据是32位长,比如0这个int型数据需要4个字节。但是如果使用LEB128格式的话,0这个数只要1个字节就可以表示了。

由于在实际应用中,我们很少接触较大的32位整数,所以LEB128数据类型能减少空间占用。那么,LEB128的格式具体是怎样的呢?来看图3-2。

图3-2 LEB128格式说明

图3-2为LEB128的格式,每个字节的第7位数据用于表示这个LEB128数据是否结束,

·第7位取值为1时表示此字节后面还有数据,也叫非结尾字节。

·第7位取值为0时表示此字节为最后一个字节,也叫结尾字节。

然后,每个字节的前7位数据再按顺序组合为一个32位数据:

·第一个字节的前7位排在最终32位数据的0到6。

·第二个字节的前7位排在7到13,以此类推。

提示 LEB128还需要区分无符号和有符号两种情况。

SLEB128:Signed LEB128,有符号的整数。结尾字节的第6位用于表示是否为负数。正负整数先转换为补码,然后按位存储在SLEB128各个字节中。SLEB128中有效数据的内容采用补码来表示。

ULEB128:Unsigned LEB128,无符号的整数。将所有字节的7位数据经过移位等组合成一个无符号32位数据。

除了ULEB128和SLEB128之外,还有一个ULEB128p1格式。其中,p是plus的意思,表示ULEB128p1需要加上1才等于ULEB128,所以这种格式的数据取值为ULEB128-1。ULEB128p1的存在使得-1这个负数只要一个字节就可以表示。

关于LEB128更详细的内容,读者可阅读参考资料[1]

提示 大小写提示

在Android文档中(参考资料[2]),这几种数据类型都用小写表示,比如uleb128、sleb128、uleb128p1。

本文及后续文章也将遵守此形式。

3.1.1.4 信息描述规则

和Class文件类似,Dex文件格式对如何使用字符串来描述成员变量和成员函数等也有要求。总体来说,Dex的使用信息描述规则和Class的使用规则大体类似,只在某些具体细节上略有不同。

提示 我们将参考官方描述 Dex文件格式Android官方介绍,https://source.android.com/devices/tech/dalvik/dex-format,Dex文件格式的官方介绍。中使用的格式来介绍字符串使用规则。

3.1.1.4.1 数据类型描述(Type Descriptor)

数据类型描述说的是用字符串表示不同的数据类型。在这方面,Dex和Class文件格式没有区别。

[数据类型描述]

(1)原始数据类型对应的字符串描述为"B","C","D","F","I","J","S","Z",它们分别对应的
   Java类型为byte,char,double,float,int,long,short,boolean。
(2)"V":表示void,不过只能用于表示函数的返回值类型。
(3)引用数据类型的格式为"LClassName;"。此处的ClassName为对应类的全路径名。
(4)数组用"[其他类型的描述名"来表示。Dex文件最多支持255维数组。
3.1.1.4.2 简短描述

在Dex文件格式中,Shorty Descriptor(简短描述)用来描述函数的参数和返回值信息,类似Class文件格式的MethodDescriptor。不过,Shorty Descriptor比MethodDescriptor要抠,省略了好些个字符。

[Shorty Descriptor]

#在Dex官方文档中,描述规则的定义和Class文件略有不同,如下:
#下面是定义ShortyDescriptor的描述规则,箭头后面是规则的组成
#注意,“()”在规则中表示一个Group,“*”号表示这个Group可以有0或多个
ShortyDescriptor → ShortyReturnType (ShortyFieldType)*
#定义ShortyReturnType的描述规则
ShortyReturnType →  'V' | ShortyFieldType
#定义ShortyFieldType的描述规则,注意,引用类型统一用"L"表示即可
ShortyFieldType → 'Z' | 'B' | 'S' |'C' | 'I' | 'J' | 'F' |      'D' |'L'

和Class文件的MethodDescriptor比较会发现:

·MethodDescriptor描述函数和返回值是"(参数类型)返回值类型",参数放在括号里。而ShortyDescriptor则是"返回值类型"+"参数类型",如果有参数就会带参数类型,没有参数就只有返回值类型。

·在ShortyDescriptor的ShortyFieldType中,引用类型只需要用"L"表示,而不需要像MethodDescriptor那样填写"L全路径类名;"。

提示 显然,ShortyDescriptor对于那些参数或返回值类型为引用类型的函数将无法区分。不过没关系,Dex中还会提供其他数据来指明参数或返回值的具体类型。这种做法的原因其实还是为了减少字符串的使用。

Dex文件和Class文件的区别还有很多,我们先介绍到这。下面直接来学习Dex文件格式。