uleb128、sleb128和uleb128p1编码格式介绍

来源:互联网 发布:csgo cpu优化 编辑:程序博客网 时间:2024/04/30 05:38

在程序中,一般使用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编码的数值(代码位于dalvik\libdex\Leb128.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编码的数值(代码同样位于dalvik\libdex\Leb128.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的编码方式。实现如下(代码位于libcore\dex\src\main\java\com\android\dex\Dex.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的整数。解码时要减一,那么反过来,编码时就需要加一。

1 0