在程序中,一般使用32位比特位来表示一个整型的数值。不过,一般能够使用到的整数值都不会太大,使用32比特位来表示就有点太浪费了。对于普通计算机来说,这没什么问题,毕竟存储空间那么大。但是,对于移动设备来说,存储空间和内存空间都非常宝贵,不能浪费,能省就省。
Android的Dalvik虚拟机中,就使用了uleb128(Unsigned Little Endian Base 128)、uleb128p1(Unsigned Little Endian Base 128 Plus 1)和sleb128(Signed Little Endian Base 128)编码来解决整形数值占用空间大小浪费的问题(在Dalvik虚拟机中只使用这三种编码来表示32位整形数值)。
首先,我们来看看uleb128编码是怎么回事。要了解原理,最简单的方法,还是阅读代码。Dalvik使用readUnsignedLeb128函数来尝试读取一个uleb128编码的数值(代码位于dalviklibdexLeb128.h中):
DEX_INLINE int readUnsignedLeb128(const u1** pStream) { const u1* ptr = *pStream; int result = *(ptr++); if (result > 0x7f) { int cur = *(ptr++); result = (result & 0x7f) | ((cur & 0x7f) << 7); if (cur > 0x7f) { cur = *(ptr++); result |= (cur & 0x7f) << 14; if (cur > 0x7f) { cur = *(ptr++); result |= (cur & 0x7f) << 21; if (cur > 0x7f) { cur = *(ptr++); result |= cur << 28; } } } } *pStream = ptr; return result; }
代码非常简单,先读取第一个字节,并判断其是否大于0x7f,如果大于的话,则代表这个字节的最高位是1,而不是0。如果是1的话,则代表还要读下一个字节;如果是0的话,则代表uleb128编码的数值到此为止。而代表原来整型值的32位数据就嵌入在每个字节当中的7比特位中(应为最高位被用来表示编码是否结束了)。所以,如果要表示的整形值非常大的话,就需要5个字节表示了(反而比原来多一个字节)。而其实uleb128可以表示数值的范围其实是要比32位整形值要大的,有三位被浪费掉了。不过,一般程序中使用的整型值都不会太大,经常是小于100的,对于这种情况来说,只需要使用一个字节就可以表示了,比普通32位整形表示法少用了3个字节,节省的空间还是非常可观的。
uleb128编码,正如其名,是小端结尾的。因此第一个字节代表的是整形值的最低7比特位的值,第二个字节代表整型值的次低7比特位的值,以此类推,最后一个字节(最高位为0)代表整形值的最高7比特位的值。所以,代码中每发现要多用一个字节,都要多向左移动7位。
在Android源码提供的文档中,有下面这张图,可以帮助理解:
这张图表示了,只使用两个字节进行编码的情况。可以看到,第一个字节的最高位为1,代表还要用到接着的下一个字节。并且,第一个字节存放的是整型值的最低7位。而第二个字节的最高位为0,代表编码到此结束,剩下的7个比特位存放了整型值的高7位数据。
综上所述,对于uleb128编码来说,其特点如下:
1)一个uleb128编码的整形值,其占用的字节数是不确定的,长度有可能在1到5个字节之间变化;
2)一个uleb128编码的整形值,是以字节中最高位是否为0来表示字节流有没有结束的。
好,我们接下来在看看sleb128编码的实现。Dalvik使用readSignedLeb128函数来读取一个sleb128编码的数值(代码同样位于dalviklibdexLeb128.h中):
DEX_INLINE int readSignedLeb128(const u1** pStream) { const u1* ptr = *pStream; int result = *(ptr++); if (result <= 0x7f) { result = (result << 25) >> 25; } else { int cur = *(ptr++); result = (result & 0x7f) | ((cur & 0x7f) << 7); if (cur <= 0x7f) { result = (result << 18) >> 18; } else { cur = *(ptr++); result |= (cur & 0x7f) << 14; if (cur <= 0x7f) { result = (result << 11) >> 11; } else { cur = *(ptr++); result |= (cur & 0x7f) << 21; if (cur <= 0x7f) { result = (result << 4) >> 4; } else { cur = *(ptr++); result |= cur << 28; } } } } *pStream = ptr; return result; }
可以看出来,其实处理的方法还是非常相似的。唯一的不同是,这里存放的是有符号整形数值,因此需要相应的进行符号位扩展,代码中是通过先左移到最左边,再算术右移同样的位数来实现的。
因此,sleb128和uleb128最大的区别就是,其编码的整形数值是有符号的,其它完全一样。
最后,我们再来看一个变种,也就是uleb128p1。这种编码方式是Dalvik独有的,如果所要表示的整数范围只包含一个负数,也就是-1的话,如果直接用sleb128编码,那就太浪费了,因此Android创造出了这种所谓的uleb128p1的编码方式。实现如下(代码位于libcoredexsrcmainjavacomandroiddexDex.java):
public final class Dex { ... public final class Section implements ByteInput, ByteOutput { ... public int readUleb128p1() { return Leb128.readUnsignedLeb128(this) - 1; } ... } ... }
其实现原理就更简单了,就是将其当做uleb128编码的值进行解码,然后再减一就可以了。因为uleb128能表示范围为0~4,294,967,295的整数,所以uleb128p1就可以表示范围为-1~4,294,967,294的整数。解码时要减一,那么反过来,编码时就需要加一。
<p>版权声明:本文为博主原创文章,未经博主允许不得转载。</p>