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的使用规则大体类似,只在某些具体细节上略有不同。
提示 我们将参考官方描述中使用的格式来介绍字符串使用规则。
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文件格式。