在Android中使用FlatBuffers(下篇)

本文來自網易雲社區數組

 

FlatBuffers編碼數組

 

編碼數組的過程以下:ide

 

 

 

先執行 startVector(),這個方法會記錄數組的長度,處理元素的對齊,準備足夠的空間,並設置nested,用於指示記錄的開始。 而後逐個添加元素。 最後 執行 endVector(),將nested復位,並記錄數組的長度。oop

 

public void startVector(int elem_size, int num_elems, int alignment) {
        notNested();
        vector_num_elems = num_elems;
        prep(SIZEOF_INT, elem_size * num_elems);
        prep(alignment, elem_size * num_elems); // Just in case alignment > int.
        nested = true;
    }

    public int endVector() {
        if (!nested)
            throw new AssertionError("FlatBuffers: endVector called without startVector");
        nested = false;
        putInt(vector_num_elems);
        return offset();
    }

咱們前面的AddressBook例子中有以下這樣的生成代碼:ui

 

public static int createPersonVector(FlatBufferBuilder builder, int[] data) {
    builder.startVector(4, data.length, 4);
    for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]);
    return builder.endVector();
  }

編碼後的數組將有以下的內存分佈:this

 

 

 

其中的Vector Length爲4字節的int型值。google

 

FlatBuffers編碼字符串

 

FlatBufferBuilder 建立字符串的過程以下:編碼

 

public int createString(CharSequence s) {
        int length = s.length();
        int estimatedDstCapacity = (int) (length * encoder.maxBytesPerChar());
        if (dst == null || dst.capacity() < estimatedDstCapacity) {
            dst = ByteBuffer.allocate(Math.max(128, estimatedDstCapacity));
        }

        dst.clear();CharBuffer src = s instanceof CharBuffer ? (CharBuffer) s :
            CharBuffer.wrap(s);
        CoderResult result = encoder.encode(src, dst, true);
        if (result.isError()) {
            try {
                result.throwException();
            } catch (CharacterCodingException x) {
                throw new Error(x);
            }
        }

        dst.flip();
        return createString(dst);
    }

    public int createString(ByteBuffer s) {
        int length = s.remaining();
        addByte((byte)0);
        startVector(1, length, 1);
        bb.position(space -= length);
        bb.put(s);
        return endVector();
    }

    public int createByteVector(byte[] arr) {
        int length = arr.length;
        startVector(1, length, 1);
        bb.position(space -= length);
        bb.put(arr);
        return endVector();
    }

編碼字符串的過程以下:url

 

  1. 對字符串進行編碼,好比 UTF-8 ,編碼後的數據保存在另外一個 ByteBuffer 中。
  2. 在可用空間的結尾處添加值爲 0 的byte。
  3. 將第 1 步中建立的 ByteBuffer 做爲一個字節數組添加到 FlatBufferBuilder 的 ByteBuffer 中。這裏不是逐個元素,也就是字節,添加,而是將 ByteBuffer 總體一次性添加,以保證字符串中各個字節的相對順序不會被顛倒過來,這一點與咱們前面在AddressBook 中看到的稍有區別。

 

編碼後的字符串將有以下的內存分佈:spa

 

 

FlatBuffers編碼對象

 

對象的編碼與數組的編碼有點相似。編碼對象的過程爲:.net

 

  1. 先執行 startObject(),建立 vtable並初始化,記錄對象的字段個數及對象數據的起始位置,並設置nested,指示對象編碼的開始。
  2. 而後爲對象逐個添加每一個字段的值。
  3. 最後執行 endObject() 結束對象的編碼。

    public void startObject(int numfields) {
         notNested();
         if (vtable == null || vtable.length < numfields) vtable = new int[numfields];
         vtable_in_use = numfields;
         Arrays.fill(vtable, 0, vtable_in_use, 0);
         nested = true;
         object_start = offset();
     }
    
     public int endObject() {
         if (vtable == null || !nested)
             throw new AssertionError("FlatBuffers: endObject called without startObject");
         addInt(0);
         int vtableloc = offset();
         // Write out the current vtable.
         for (int i = vtable_in_use - 1; i >= 0 ; i--) {
             // Offset relative to the start of the table.
             short off = (short)(vtable[i] != 0 ? vtableloc - vtable[i] : 0);
             addShort(off);
         }
    
         final int standard_fields = 2; // The fields below:
         addShort((short)(vtableloc - object_start));
         addShort((short)((vtable_in_use + standard_fields) * SIZEOF_SHORT));
    
         // Search for an existing vtable that matches the current one.
         int existing_vtable = 0;
         outer_loop:
         for (int i = 0; i < num_vtables; i++) {
             int vt1 = bb.capacity() - vtables[i];
             int vt2 = space;
             short len = bb.getShort(vt1);
             if (len == bb.getShort(vt2)) {
                 for (int j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) {
                     if (bb.getShort(vt1 + j) != bb.getShort(vt2 + j)) {
                         continue outer_loop;
                     }
                 }
                 existing_vtable = vtables[i];
                 break outer_loop;
             }
         }
    
         if (existing_vtable != 0) {
             // Found a match:
             // Remove the current vtable.
             space = bb.capacity() - vtableloc;
             // Point table to existing vtable.
             bb.putInt(space, existing_vtable - vtableloc);
         } else {
             // No match:
             // Add the location of the current vtable to the list of vtables.
             if (num_vtables == vtables.length) vtables = Arrays.copyOf(vtables, num_vtables * 2);
             vtables[num_vtables++] = offset();
             // Point table to current vtable.
             bb.putInt(bb.capacity() - vtableloc, offset() - vtableloc);
         }
    
         nested = false;
         return vtableloc;
     }

    結束對象編碼的過程比較有意思:

  4. 在可用空間的結尾處添加值爲 0 的int。
  5. 記錄下當前的offset值 vtableloc,也就是 ByteBuffer中已經保存的數據的長度。
  6. 編碼vtable。vtable用於記錄對象每一個字段的存儲位置,在爲對象添加字段時會被更新。在這裏會用 vtableloc - vtable[i],找到每一個對象的保存位置相對於對象起始位置的偏移,並將這個偏移量保存到ByteBuffer中。
  7. 記錄對象全部字段的總長度,包含對象開始初值爲0的int數據。
  8. 記錄元數據的長度。這包括vtable的長度,記錄 對象全部字段的總長度 的short型值,以及這個長度自己所消耗的存儲空間。
  9. 查找是否有一個vtable與正在建立的這個一致。
  10. 找到了匹配的vtable,則清除建立的元數據。第 1 步中放0的那個位置的值,被更新爲找到的vtable相對於對象的數據起始位置的偏移。
  11. 沒有找到匹配的vtable。記下vtable的位置,第 1 步中放0的那個位置的值,被更新爲新建立的vtable相對於對象的數據起始位置的偏移。

 

就像C++中的vtable,這裏的vtable也是針對類建立的,而不是對象。

編碼後的對象有以下的內存分佈:

 

 

 

圖中值爲0的那個位置的值實際不是0,它指向vtable,圖中是指向在建立對象時建立的vtable,但它也能夠相同類已經存在的vtable。

 

結束編碼

 

編碼數據以後,須要執行 FlatBufferBuilder 的 finish() 結束編碼:

 

public int offset() {
        return bb.capacity() - space;
    }

    public void addOffset(int off) {
        prep(SIZEOF_INT, 0);  // Ensure alignment is already done.
        assert off <= offset();
        off = offset() - off + SIZEOF_INT;
        putInt(off);
    }

    public void finish(int root_table) {
        prep(minalign, SIZEOF_INT);
        addOffset(root_table);
        bb.position(space);
        finished = true;
    }

    public void finish(int root_table, String file_identifier) {
        prep(minalign, SIZEOF_INT + FILE_IDENTIFIER_LENGTH);
        if (file_identifier.length() != FILE_IDENTIFIER_LENGTH)
            throw new AssertionError("FlatBuffers: file identifier must be length " +
                                     FILE_IDENTIFIER_LENGTH);
        for (int i = FILE_IDENTIFIER_LENGTH - 1; i >= 0; i--) {
            addByte((byte)file_identifier.charAt(i));
        }
        finish(root_table);
    }

這個方法主要是記錄根對象的位置。給 finish() 傳入的的根對象的位置是相對於ByteBuffer結尾處的偏移,可是在 addOffset() 中,這個偏移會被轉換爲相對於整個數據塊開始處的偏移。計算off值時,最後加的SIZEOF_INT是要給後面放入的off留出空間。

 

整個編碼後的數據有以下的內存分佈:

 

 

FlatBuffers 解碼原理

 

這裏咱們經過一個生成的比較簡單的類 PhoneNumber 來了解FlatBuffers的解碼。

 

public static PhoneNumber getRootAsPhoneNumber(ByteBuffer _bb) {
        return getRootAsPhoneNumber(_bb, new PhoneNumber());
    }

    public static PhoneNumber getRootAsPhoneNumber(ByteBuffer _bb, PhoneNumber obj) {
        _bb.order(ByteOrder.LITTLE_ENDIAN);
        return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb));
    }

    public void __init(int _i, ByteBuffer _bb) {
        bb_pos = _i;
        bb = _bb;
    }

    public PhoneNumber __assign(int _i, ByteBuffer _bb) {
        __init(_i, _bb);
        return this;
    }

建立對象的時候,會初始化 bb 爲保存有對象數據的ByteBuffer,bb_pos 爲對象數據在ByteBuffer中的偏移。在 getRootAsPhoneNumber() 中會從 ByteBuffer的position處獲取根對象的偏移,並加上position,以計算出對象在ByteBuffer中的位置。

 

經過生成的PhoneNumber類中的number()、type()兩個方法來看, FlatBuffers 中是怎麼訪問成員的:

 

public String number() {
        int o = __offset(4);
        return o != 0 ? __string(o + bb_pos) : null;
    }

    public int type() {
        int o = __offset(6);
        return o != 0 ? bb.getInt(o + bb_pos) : 0;
    }

過程大致爲:

 

  1. 得到對應字段在對象中的偏移位置。
  2. 根據字段的偏移位置及對象的原點位置計算出對象的位置。
  3. 經過ByteBuffer等提供的一些方法獲得字段的值。

 

計算字段相對於對象原點位置的偏移的方法 __offset(4) 在com.google.flatbuffers.Table中定義:

 

protected int __offset(int vtable_offset) {
    int vtable = bb_pos - bb.getInt(bb_pos);
    return vtable_offset < bb.getShort(vtable) ? bb.getShort(vtable + vtable_offset) : 0;
  }

在這個方法中,先是根據對象的原點處保存的vtable的偏移獲得vtable的位置,而後在從vtable中獲取對象字段相對於對象原點位置的偏移。

 

獲得字符串字段的過程以下:

protected String __string(int offset) {
    CharsetDecoder decoder = UTF8_DECODER.get();
    decoder.reset();

    offset += bb.getInt(offset);
    ByteBuffer src = bb.duplicate().order(ByteOrder.LITTLE_ENDIAN);
    int length = src.getInt(offset);
    src.position(offset + SIZEOF_INT);
    src.limit(offset + SIZEOF_INT + length);

    int required = (int)((float)length * decoder.maxCharsPerByte());
    CharBuffer dst = CHAR_BUFFER.get();
    if (dst == null || dst.capacity() < required) {
      dst = CharBuffer.allocate(required);
      CHAR_BUFFER.set(dst);
    }

    dst.clear();

    try {
      CoderResult cr = decoder.decode(src, dst, true);
      if (!cr.isUnderflow()) {
        cr.throwException();
      }
    } catch (CharacterCodingException x) {
      throw new Error(x);
    }

    return dst.flip().toString();
  }

瞭解了前面字符串編碼的過程以後,相信也不難了解這裏解碼字符串的過程,這裏徹底是那個過程的相反過程。

 

如咱們所見,FlatBuffers編碼後的數據其實無需解碼,只要經過生成的Java類對這些數據進行解釋就能夠了。

 

FlatBuffers的原理大致如此。

 

Done。

 

 

相關閱讀:

在Android中使用FlatBuffers(上篇)

在Android中使用FlatBuffers(中篇)

在Android中使用FlatBuffers(下篇)

網易雲新用戶大禮包:https://www.163yun.com/gift

本文來自網易雲社區,經做者韓鵬飛受權發佈。

相關文章
相關標籤/搜索