uleb128、sleb128和uleb128p1編碼格式介紹

在程序中,一般使用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的整數。解碼時要減一,那麼反過來,編碼時就需要加一。

發佈了36 篇原創文章 · 獲贊 55 · 訪問量 34萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章