本文來自網易雲社區。數組
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
- 對字符串進行編碼,好比 UTF-8 ,編碼後的數據保存在另外一個 ByteBuffer 中。
- 在可用空間的結尾處添加值爲 0 的byte。
- 將第 1 步中建立的 ByteBuffer 做爲一個字節數組添加到 FlatBufferBuilder 的 ByteBuffer 中。這裏不是逐個元素,也就是字節,添加,而是將 ByteBuffer 總體一次性添加,以保證字符串中各個字節的相對順序不會被顛倒過來,這一點與咱們前面在AddressBook 中看到的稍有區別。
編碼後的字符串將有以下的內存分佈:spa
FlatBuffers編碼對象
對象的編碼與數組的編碼有點相似。編碼對象的過程爲:.net
- 先執行 startObject(),建立 vtable並初始化,記錄對象的字段個數及對象數據的起始位置,並設置nested,指示對象編碼的開始。
- 而後爲對象逐個添加每一個字段的值。
-
最後執行 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; }
結束對象編碼的過程比較有意思:
- 在可用空間的結尾處添加值爲 0 的int。
- 記錄下當前的offset值 vtableloc,也就是 ByteBuffer中已經保存的數據的長度。
- 編碼vtable。vtable用於記錄對象每一個字段的存儲位置,在爲對象添加字段時會被更新。在這裏會用 vtableloc - vtable[i],找到每一個對象的保存位置相對於對象起始位置的偏移,並將這個偏移量保存到ByteBuffer中。
- 記錄對象全部字段的總長度,包含對象開始初值爲0的int數據。
- 記錄元數據的長度。這包括vtable的長度,記錄 對象全部字段的總長度 的short型值,以及這個長度自己所消耗的存儲空間。
- 查找是否有一個vtable與正在建立的這個一致。
- 找到了匹配的vtable,則清除建立的元數據。第 1 步中放0的那個位置的值,被更新爲找到的vtable相對於對象的數據起始位置的偏移。
- 沒有找到匹配的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; }
過程大致爲:
- 得到對應字段在對象中的偏移位置。
- 根據字段的偏移位置及對象的原點位置計算出對象的位置。
- 經過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。
相關閱讀:
網易雲新用戶大禮包:https://www.163yun.com/gift
本文來自網易雲社區,經做者韓鵬飛受權發佈。