深入淺出 FlatBuffers 之 Schema

一縷殤流化隱半邊冰霜發表於2018-07-02

一. FlatBuffers 是什麼?

FlatBuffers 是一個序列化開源庫,實現了與 Protocol Buffers,Thrift,Apache Avro,SBE 和 Cap'n Proto 類似的序列化格式,主要由 Wouter van Oortmerssen 編寫,並由 Google 開源。Oortmerssen 最初為 Android 遊戲和注重效能的應用而開發了FlatBuffers。現在它具有C ++,C#,C,Go,Java,PHP,Python 和 JavaScript 的埠。

FlatBuffer 是一個二進位制 buffer,它使用 offset 組織巢狀物件(struct,table,vectors,等),可以使資料像任何基於指標的資料結構一樣,就地訪問資料。然而 FlatBuffer 與大多數記憶體中的資料結構不同,它使用嚴格的對齊規則和位元組順序來確保 buffer 是跨平臺的。此外,對於 table 物件,FlatBuffers 提供前向/後向相容性和 optional 欄位,以支援大多數格式的演變。

FlatBuffers 的主要目標是避免反序列化。這是通過定義二進位制資料協議來實現的,一種將定義好的將資料轉換為二進位制資料的方法。由該協議建立的二進位制結構可以 wire 傳送,並且無需進一步處理即可讀取。相比較而言,在傳輸 JSON 時,我們需要將資料轉換為字串,通過 wire 傳送,解析字串,並將其轉換為本地物件。Flatbuffers 不需要這些操作。你用二進位制裝入資料,傳送相同的二進位制檔案,並直接從二進位制檔案讀取。

儘管 FlatBuffers 有自己的介面定義語言來定義要與之序列化的資料,但它也支援 Protocol Buffers 中的 .proto格式。

在 schema 中定義物件型別,然後可以將它們編譯為 C++ 或 Java 等各種主流語言,以實現零開銷讀寫。FlatBuffers 還支援將 JSON 資料動態地分析到 buffer 中。

除了解析效率以外,二進位制格式還帶來了另一個優勢,資料的二進位制表示通常更具有效率。我們可以使用 4 位元組的 UInt 而不是 10 個字元來儲存 10 位數字的整數。

二. 為什麼要發明 FlatBuffers ?

深入淺出 FlatBuffers 之 Schema

JSON 是一種獨立於語言存在的資料格式,但是它解析資料並將之轉換成如 Java 物件時,會消耗我們的時間和記憶體資源。客戶端解析一個 20KB 的 JSON 流差不多需要 35ms,而 UI 一次重新整理的時間是 16.6ms。在高實時遊戲中,是不能有任何卡頓延遲的,所以需要一種新的資料格式;伺服器在解析 JSON 時候,有時候會建立非常多的小物件,對於每秒要處理百萬玩家的 JSON 資料,伺服器壓力會變大,如果每次解析 JSON 都會產生很多小物件,那麼海量玩家帶來的海量小物件,在記憶體回收的時候可能會造成 GC 相關的問題。Google 員工 Wouter van Oortmerssen 為了解決遊戲中效能的問題,於是開發出了 FlatBuffers。(注:Protocol buffers 是 created by google,而 FlatBuffers 是 created at google)

幾年前,Facebook 宣稱自己的 Android app 在資料處理的效能方面有了極大的提升。在幾乎整個 app 中,他們放棄了 JSON 而用 FlatBuffers 取而代之。

FlatBuffers (9490 star) 和 Cap'n Proto (5527 star)、simple-binary-encoding (1351 star) 一樣,它支援“零拷貝”反序列化,在序列化過程中沒有臨時物件產生,沒有額外的記憶體分配,訪問序列化資料也不需要先將其複製到記憶體的單獨部分,這使得以這些格式訪問資料比需要格式的資料(如JSON,CSV 和 protobuf)快得多。

FlatBuffers 與 Protocol Buffers 確實比較相似,主要的區別在於 FlatBuffers 在訪問資料之前不需要解析/解包。兩者程式碼也是一個數量級的。但是 Protocol Buffers 既沒有可選的文字匯入/匯出功能,也沒有 union 這個語言特性,這兩點 FlatBuffers 都有。

FlatBuffers 專注於移動硬體(記憶體大小和記憶體頻寬比桌面端硬體更受限制),以及具有最高效能需求的應用程式:遊戲。

三. FlatBuffers 使用量

說了這麼多,讀者會疑問,FlatBuffers 使用的人多麼?Google 官方頁面上提了 3 個著名的 app 和 1 個框架在使用它。

BobbleApp,印度第一貼圖 App。BobbleApp 中使用 FlatBuffers 後 App 的效能明顯增強。

Facebook 使用 FlatBuffers 在 Android App 中進行客戶端服務端的溝通。他們寫了一篇文章《Improving Facebook's performance on Android with FlatBuffers》來描述 FlatBuffers 是如何加速載入內容的。

Google 的 Fun Propulsion Labs 在他們所有的庫和遊戲中大量使用 FlatBuffers。

Cocos2d-X,第一開源移動遊戲引擎,使用 FlatBuffers 來序列化所有的遊戲資料。

由此可見,在遊戲類的 app 中,廣泛使用 FlatBuffers。

四. 定義 .fbs schema 檔案

深入淺出 FlatBuffers 之 Schema

編寫一個 schema 檔案,允許您定義您想要序列化的資料結構。欄位可以有標量型別(所有大小的整數/浮點數),也可以是字串,任何型別的陣列,引用另一個物件,或者一組可能的物件(Union)。欄位可以是可選 optional 的也可以有預設值,所以它們不需要存在於每個物件例項中。

舉個例子:

// example IDL file

namespace MyGame;

attribute "priority";

enum Color : byte { Red = 1, Green, Blue }

union Any { Monster, Weapon, Pickup }

struct Vec3 {
  x:float;
  y:float;
  z:float;
}

table Monster {
  pos:Vec3;
  mana:short = 150;
  hp:short = 100;
  name:string;
  friendly:bool = false (deprecated, priority: 1);
  inventory:[ubyte];
  color:Color = Blue;
  test:Any;
}

root_type Monster;
複製程式碼

上面是 schema 語言的語法,schema 又名 IDL(Interface Definition Language,介面定義語言),程式碼和 C 家族的語言非常像。

在 FlatBuffers 的 schema 檔案中,有兩個非常重要的概念,struct 和 table 。

1. Table

Table 是在 FlatBuffers 中定義物件的主要方式,由一個名稱(這裡是 Monster)和一個欄位列表組成。每個欄位都有一個名稱,一個型別和一個可選的預設值(如果省略,它預設為 0 / NULL)。

Table 中每個欄位都是可選 optional 的:它不必出現在 wire 表示中,並且可以選擇省略每個單獨物件的欄位。因此,您可以靈活地新增欄位而不用擔心資料膨脹。這種設計也是 FlatBuffer 的前向和後向相容機制。

假設當前 schema 是如下:

table { a:int; b:int; }
複製程式碼

現在想對這個 schema 進行更改。

有幾點需要注意:

新增欄位

只能在表定義的末尾新增新的欄位。舊資料仍會正確讀取,並在讀取時為您提供預設值。舊程式碼將簡單地忽略新欄位。如果希望靈活地使用 schema 中欄位的任何順序,您可以手動分配 ids(很像 Protocol Buffers),請參閱下面的 id 屬性

舉例:

table { a:int; b:int; c:int; }
複製程式碼

這樣做可以。舊的 schema 讀取新的資料結構會忽略新欄位 c 的存在。新的 schema 讀取舊的資料,將會取到 c 的預設值(在此情況下為 0,因為未指定)。

table { c:int a:int; b:int; }
複製程式碼

在前面新增新欄位是不允許的,因為這會使 schema 新舊版本不相容。用老的程式碼讀取新的資料,讀取新欄位 c 的時候,其實讀到的是老的 a 欄位。用新程式碼讀取老的資料,讀取老欄位 a 的時候,其實讀到的是老的 b 欄位。

table { c:int (id: 2); a:int (id: 0); b:int (id: 1); }
複製程式碼

這樣做是可行的。如果您的意圖是以有意義的方式對語義進行排序/分組,您可以使用顯式標識賦值來完成。引入 id 以後,table 中的欄位順序就無所謂了,新的與舊的 schema 完全相容,只要我們保留 id 序列即可。

刪除欄位

不能從 schema 中刪除不再使用的欄位,但可以簡單地停止將它們寫入資料中,和寫入和刪除欄位,兩種做法幾乎相同的效果。此外,可以將它們標記為 deprecated,如上例所示,被標記的欄位不會再生成 C ++ 的訪問器,從而強制該欄位不再被使用。 (小心:這可能會破壞程式碼!)。

table { b:int; }
複製程式碼

這種刪除欄位的方法不可行。我們只能通過棄用來刪除某個欄位,而不管是否使用了明確的ID 標識。

table { a:int (deprecated); b:int; }
複製程式碼

上面這樣的做法也是可以的。舊的 schema 讀取新的資料結構會獲得 a 的預設值,因為它不存在。新的 schema 程式碼不能讀取也不能寫入 a(現有程式碼嘗試這樣做會導致編譯錯誤),但仍可以讀取舊資料(它們將忽略該欄位)。

更改欄位

可以更改欄位名稱和 table 名稱,如果您的程式碼可以正常工作,那麼您也可以更改它們。

table { a:uint; b:uint; }
複製程式碼

直接修改欄位的型別,這樣做可能可行,也有情況不行。只有在型別改變是相同大小的情況下,是可行的。如果舊資料不包含任何負數,這將是安全的,如果包含了負數,這樣改變會出現問題。

table { a:int = 1; b:int = 2; }
複製程式碼

這樣修改不可行。任何寫入數值為 0 的舊資料都不會再寫入 buffer,並依賴於重新建立的預設值。現在這些值將顯示為1和2。有些情況下可能不會出錯,但必須小心。

table { aa:int; bb:int; }
複製程式碼

上面這種修改方法,修改原來的變數名以後,可能會出現問題。由於已經重新命名了欄位,這將破壞所有使用此版本 schema 的程式碼(和 JSON 檔案),這與實際的二進位制緩衝區不相容。

table 是 FlatBuffers 的基石,因為對於大多數需要序列化應用來說,資料結構改變是必不可少的。通常情況下,處理資料結構的變更在大多數序列化解決方案的解析過程中可以透明地完成的。但是一個 FlatBuffer 在被訪問之前不會被分析。

為了解決資料結構變更的問題,table 通過 vtable 間接訪問欄位。每個 table 都帶有一個 vtable(可以在具有相同佈局的多個 table 之間共享),並且包含儲存此特定型別 vtable 例項的欄位的資訊。vtable 還可能表明該欄位不存在(因為此 FlatBuffer 是使用舊版本的軟體編寫的,僅僅因為資訊對於此例項不是必需的,或者被視為已棄用),在這種情況下會返回預設值。

table 的記憶體開銷很小(因為 vtables 很小並且共享)訪問成本也很小(間接訪問),但是提供了很大的靈活性。table 甚至可能比等價的 struct 花費更少的記憶體,因為欄位在等於預設值時不需要儲存在 buffer 中。

2. Structs

structs 和 table 非常相似,只是 structs 沒有任何欄位是可選的(所以也沒有預設值),欄位可能不會被新增或被棄用。結構可能只包含標量或其他結構。如果確定以後不會進行任何更改(如 Vec3 示例中非常明顯),請將其用於簡單物件。structs 使用的記憶體少於 table,並且訪問速度更快(它們總是以串聯方式儲存在其父物件中,並且不使用虛擬表)。

structs 不提供前向/後向相容性,但佔用記憶體更小。對於不太可能改變的非常小的物件(例如座標對或RGBA顏色)存成 struct 是非常有用的。

3. Types

FlatBuffers 支援的 標量 型別有以下幾種:

  • 8 bit: byte (int8), ubyte (uint8), bool
  • 16 bit: short (int16), ushort (uint16)
  • 32 bit: int (int32), uint (uint32), float (float32)
  • 64 bit: long (int64), ulong (uint64), double (float64)

括號裡面的名字對應的是型別的別名。

FlatBuffers 支援的 非標量 型別有以下幾種:

  • 任何型別的陣列。不過不支援巢狀陣列,可以用 table 內定義陣列的方式來取代巢狀陣列。
  • UTF-8 和 7-bit ASCII 的字串。其他格式的編碼字串或者二進位制資料,需要用 [byte] 或者 [ubyte] 來替代。
  • table、structs、enums、unions

標量型別的欄位有預設值,非標量的欄位(string/vector/table)如果沒有值的話,預設值為 NULL。

一旦一個型別宣告瞭,儘量不要改變它的型別,一旦改變了,很可能就會出現錯誤。上面也提到過了,如果把 int 改成 uint,資料如果有負數,那麼就會出錯。

4. Enums

定義一系列命名常量,每個命名常量可以分別給一個定值,也可以預設的從前一個值增加一。預設的第一個值是 0。正如在上面例子中看到的列舉宣告,使用:(上面例子中是 byte 位元組)指定列舉的基本整型,然後確定用這個列舉型別宣告的每個欄位的型別。

通常,只應新增列舉值,不要去刪除列舉值(對列舉不存在棄用一說)。這需要開發者程式碼通過處理未知的列舉值來自行處理向前相容性的問題

5. Unions

這個是 Protocol buffers 中還不支援的型別

union 是 C 語言中的概念,一個 union 中可以放置多種型別,共同使用一個記憶體區域。

但是在 FlatBuffers 中,Unions 可以像 Enums 一樣共享許多屬性,但不是常量的新名稱,而是使用 table 的名稱。可以宣告一個 Unions 欄位,該欄位可以包含對這些型別中的任何一個的引用,即這塊記憶體區域只能由其中一種型別使用。另外還會生成一個帶有字尾 _type 的隱藏欄位,該欄位包含相應的列舉值,從而可以在執行時知道要將哪些型別轉換為型別。

union 跟 enum 比較類似,但是 union 包含的是 table,enum 包含的是 scalar或者 struct。

Unions 是一種能夠在一個 FlatBuffer 中傳送多種訊息型別的好方法。請注意,因為union 欄位實際上是兩個欄位(有一個隱藏欄位),所以它必須始終是表的一部分,它本身不能作為 FlatBuffer 的 root。

如果需要以更開放的方式區分不同的 FlatBuffers,例如檔案,請參閱下面的檔案標識功能

最後還有一個實驗功能,只在 C++ 的版本實現中提供支援,如上面例子中,把 [Any] (聯合體陣列) 作為一個型別新增到了 Monster 的 table 定義中。

6. Root type

這宣告瞭您認為是序列化資料的根表(或結構)。這對於解析不包含物件型別資訊的 JSON 資料尤為重要。

7. File identification and extension

通常情況下,FlatBuffer 二進位制緩衝區不是自描述的,即它需要您瞭解其 schema 才能正確解析資料。但是如果你想使用一個 FlatBuffer 作為檔案格式,那麼能夠在那裡有一個“魔術數字”是很方便的,就像大多數檔案格式一樣,能夠做一個完整的檢查來看看你是否閱讀你期望的檔案型別。

FlatBuffer 雖然允許開發者可以在 FlatBuffer 前加上自己的檔案頭,但 FlatBuffers 有一種內建方法,可以讓識別符號佔用最少空間,並且還能使 FlatBuffer 與不具有此類識別符號的 FlatBuffer 相互相容。

宣告檔案格式的方法類似於 root_type:

file_identifier "MYFI";
複製程式碼

識別符號必須正好 4 個字元。這 4 個字元將作為 buffer 末尾的 [4,7] 位元組

對於具有這種識別符號的任何 schema,flatc 會自動將識別符號新增到它生成的任何二進位制檔案中(帶-b),並且生成的呼叫如 FinishMonsterBuffer 也會新增識別符號。如果你已經指定了一個識別符號並希望生成一個沒有識別符號的緩衝區,你可以通過直接顯示呼叫FlatBufferBuilder :: Finish 來完成這一目的。

載入緩衝區資料以後,可以使用像 MonsterBufferHasIdentifier 這樣的呼叫來檢查識別符號是否存在。

給檔案新增識別符號是最佳實踐。如果只是簡單的想通過網路傳送一組可能的訊息中的一個,那麼最好用 Union。

預設情況下,flatc 會將二進位制檔案輸出為 .bin。schema 中的這個宣告會將其改變為任何你想要的:

file_extension "ext";
複製程式碼

8. RPC interface declarations

RPC 宣告瞭一組函式,它將 FlatBuffer 作為入參(request)並返回一個 FlatBuffer 作為 response(它們都必須是 table 型別):

rpc_service MonsterStorage {
  Store(Monster):StoreResponse;
  Retrieve(MonsterId):Monster;
}
複製程式碼

這些產生的程式碼以及它的使用方式取決於使用的語言和 RPC 系統,可以通過增加 --grpc 編譯引數,程式碼生成器會對 GRPC 有初步的支援。

9. Attributes

Attributes 可以附加到欄位宣告,放在欄位後面或者 table/struct/enum/union 的名稱之後。這些欄位可能有值也有可能沒有值。

一些 Attributes 只能被編譯器識別,比如 deprecated。使用者也可以定義一些 Attributes,但是需要提前進行 Attributes 宣告。宣告以後可以在執行時解析 schema 的時候進行查詢。這個對於開發一個屬於自己的程式碼編譯/生成器來說是非常有用的。或者是想新增一些特殊資訊(一些幫助資訊等等)到自己的 FlatBuffers 工具之中。

目前最新版能識別到的 Attributes 有 11 種。

  • id:n (on a table field)
    id 代表設定某個欄位的識別符號為 n 。一旦啟用了這個 id 識別符號,那麼所有欄位都必須使用 id 標識,並且 id 必須是從 0 開始的連續數字。需要特殊注意的是 Union,由於 Union 是由 2 個欄位構成的,並且隱藏欄位是排在 union 欄位的前面。(假設在 union 前面欄位的 id 排到了6,那麼 union 將會佔據 7 和 8 這兩個 id 編號,7 是隱藏欄位,8 是 union 欄位)新增了 id 識別符號以後,欄位在 schema 內部的相互順序就不重要了。新欄位用的 id 必須是緊接著的下一個可用的 id(id 不能跳,必須是連續的)。
  • deprecated (on a field)
    deprecated 代表不再為此欄位生成訪問器,程式碼應停止使用此資料。舊資料可能仍包含此欄位,但不能再通過新的程式碼去訪問這個欄位。請注意,如果您棄用先前所需的欄位,舊程式碼可能無法驗證新資料(使用可選驗證器時)。
  • required (on a non-scalar table field)
    required 代表該欄位不能被省略。預設情況下,所有欄位都是可選的,即可以省略。這是可取的,因為它有助於向前/向後相容性以及資料結構的靈活性。這也是閱讀程式碼的負擔,因為對於非標量欄位,它要求您檢查 NULL 並採取適當的操作。通過指定 required 欄位,可以強制構建 FlatBuffers 的程式碼確保此欄位已初始化,因此讀取的程式碼可以直接訪問它,而不檢查 NULL。如果構造程式碼沒有初始化這個欄位,他們將得到一個斷言,並提示缺少必要的欄位。請注意,如果將此屬性新增到現有欄位,則只有在現有資料始終包含此欄位/現有程式碼始終寫入此欄位,這兩種情況下才有效。
  • force_align: size (on a struct)
    force_align 代表強制這個結構的對齊比它自然對齊的要高。如果 buffer 建立的時候是以 force_align 宣告建立的,那麼裡面的所有 structs 都會被強制對齊。(對於在 FlatBufferBuilder 中直接訪問的緩衝區,這種情況並不是一定的)
  • bit_flags (on an enum)
    bit_flags 這個欄位的值表示位元,這意味著在 schema 中指定的任何值 N 最終將代表1 << N,或者預設不指定值的情況下,將預設得到序列1,2,4,8 ,...
  • nested_flatbuffer: "table_name" (on a field)
    nested_flatbuffer 代表該欄位(必須是 ubyte 的陣列)巢狀包含 flatbuffer 資料,其根型別由 table_name 給出。生成的程式碼將為巢狀的 FlatBuffer 生成一個方便的訪問器。
  • flexbuffer (on a field)
    flexbuffer 表示該欄位(必須是 ubyte 的陣列)包含 flexbuffer 資料。生成的程式碼將為 FlexBuffer 的 root 建立一個方便的訪問器。
  • key (on a field)
    key 欄位用於當前 table 中,對其所在型別的陣列進行排序時用作關鍵字。可用於就地查詢二進位制搜尋。
  • hash (on a field)
    這是一個不帶符號的 32/64 位整數字段,因為在 JSON 解析過程中它的值允許為字串,然後將其儲存為其雜湊。屬性的值是要使用的雜湊演算法,即使用 fnv1_32、fnv1_64、fnv1a_32、fnv1a_64 其中之一。
  • original_order (on a table)
    由於表中的元素不需要以任何特定的順序儲存,因此通常為了優化空間,而對它們大小進行排序。而 original_order 阻止了這種情況發生。通常應該沒有任何理由使用這個標誌。
  • 'native_*'
    已經新增了幾個屬性來支援基於 C++ 物件的 API,所有這些屬性都以 “native_” 作為字首。具體可以點連結檢視支援的說明,native_inlinenative_defaultnative_custom_allocnative_typenative_include: "path"

10. 設計建議

FlatBuffers 是一個高效的資料格式,但要實現效率,您需要一個高效的 schema。如何表示具有完全不同 size 大小特徵的資料通常有多種選擇。

由於 FlatBuffers 的靈活性和可擴充套件性,將任何型別的資料表示為字典(如在 JSON 中)是非常普遍的做法。儘管可以在 FlatBuffers(作為具有鍵和值的表的陣列)中模擬這一點,但這對於像 FlatBuffers 這樣的強型別系統來說,這樣做是一種低效的方式,會導致生成相對較大的二進位制檔案。在大多數系統中,FlatBuffer table 比 classes/structs 更靈活,因為 table 在處理 field 數量非常多,但是實際使用只有其中少數幾個 field 這種情況,效率依舊非常高。因此,組織資料應該儘可能的組織成 table 的形式。

同樣,如果可能的話,儘量使用列舉的形式代替字串。

FlatBuffers 中沒有繼承的概念,所以想表示一組相關資料結構的方式是 union。但是,union 確實有成本,另外一種高效的做法就是建立一個 table 。如果這些資料結構有很多相似或者可以共享的 field ,那麼建議一個 table 是非常高效的。在這個 table 中包含所有資料結構的所有欄位即可。高效的原因就是 optional 欄位是非常廉價的,消耗少。

FlatBuffers 預設可以支援存放的下所有整數,因此儘量選擇所需的最小大小,而不是預設為 int/long。

可以考慮用 buffer 中一個字串或者 table 來共享一些公共的資料,這樣做會提高效率,因此將重複的資料拆成共享資料結構 + 私有資料結構,這樣做是非常值得的。

五. FlatBuffers 的 JSON 解析

深入淺出 FlatBuffers 之 Schema

FlatBuffers 是支援解析 JSON 成自己的格式的。即解析 schema 的解析器同樣可以解析符合 schema 規則的 JSON 物件。所以和其他的 JSON 解析器不同,這個解析器是強型別的,並且解析結果也只是 FlatBuffers。具體做法請參照 flatc 文件和 C++ 對應的 FlatBuffers 文件,檢視如何在執行時解析 JSON 成 FlatBuffers。

為了解析 JSON,除了需要定義一個 schema 以外,FlatBuffers 的解析器還有以下這些改變:

  • 它接受帶和不帶引號的欄位名稱,就像許多 JSON 解析器已經做的那樣。它也可以不用引號輸出它們,但可以使用 strict_json 標誌輸出它們。
  • 如果一個欄位具有列舉型別,解析器會將列舉識別符號列舉值(帶或不帶引號)而不是數字,例如 field:EnumVal。如果一個欄位是整數型別的,你仍然可以使用符號名稱,但是這些值需要以它們的型別作為字首,並且需要用引號引起來。field:“Enum.EnumVal”。對於代表標誌的列舉,可以在多個字串中插入空格或者利用點語法,例如。field:“EnumVal1 EnumVal2” 或 field:“Enum.EnumVal1 Enum.EnumVal2”。
  • 對於 union,這些需要用兩個 field 來指定,就像在從程式碼序列化時一樣。例如。對於 field foo,您必須在 foo 欄位之前新增一個 foo_type:FooOne,FooOne 就是可以在 union 之外使用的 table。
  • 如果一個 field 的值是 null(例如,field:null)意味著這個欄位是有預設值的(與完全未指定該欄位,這兩種情況具有相同的效果)。
  • 解析器內建了一些轉換函式,所以你可以用 rad(180) 函式替代寫 3.14159 的地方。目前支援以下這些函式:rad,deg,cos,sin,tan,acos,asin,atan。

解析JSON時,解析器識別字串中的以下轉義碼:

\n - 換行。
\t - 標籤。
\r - 回車。
\b - 退格。
\f - 換頁。
\“ - 雙引號。
\\ - 反斜槓。
\/ - 正斜槓。
\uXXXX - 16位 unicode,轉換為等效的 UTF-8 表示。
\xXX - 8 位二進位制十六進位制數字 XX。這是唯一一個不屬於 JSON 規範的地方(請參閱json.org/),但是需要能夠將字串中的任意二進位制編碼為文字並返回而不丟失資訊(例如位元組 0xFF 就不可以表示為標準的 JSON)。

當從二進位制再反向表示生成 JSON 時,它還會再次生成這些轉義程式碼。

六. FlatBuffers 命名規範

schema 中的識別符號是為了翻譯成許多不同的程式語言,所以把 schema 的編碼風格改成和當前專案語言使用的風格,是一種錯誤的做法。應該讓 schema 的程式碼風格更加通用。

  • Table, struct, enum and rpc names (types) 採用大寫駝峰命名法。
  • Table and struct field names 採用下劃線命名法。這樣做方法自動生成小寫駝峰命名的程式碼。
  • Enum values 採用大寫駝峰命名法。
  • namespaces 採用大寫駝峰命名法。

還有 2 條關於書寫格式的建議:

  • 大括號:與宣告的開頭位於同一行。
  • 間距:縮排2個空格。:兩邊沒有空格,=兩邊各一個空格。

七. FlatBuffers 一些"坑"

深入淺出 FlatBuffers 之 Schema

大多數可序列化格式(例如 JSON 或 Protocol Buffers)對於某個欄位是否存在於某個物件中是非常明確,可以將其用作“額外”資訊。

但是在 FlatBuffers 中,除了標量值之外,這也適用於其他所有內容。 FlatBuffers 預設情況下不會寫入等於預設值的欄位(對於標量),這樣可以節省大量空間。 然而,這也意味著測試一個欄位是否“存在”有點沒有意義,因為它不會告訴你,該欄位是否是通過呼叫add_field 方法調來 set 的,除非你對非預設值的資訊感興趣。預設值是不會寫入到 buffer 中的。

可變的 FlatBufferBuilder 實現了一個名為 force_defaults 的方法,可以避免這種行為,因為即使與預設值相等,也會寫入欄位。然後可以使用 IsFieldPresent 來查詢 buffer 中是否存在某個欄位。

另一種方法是將標量欄位包裝在 struct 中。這樣,如果它不存在,它將返回 null。這種方法厲害的是,struct 不會佔用比它們所代表的標量更多的空間。

八. 最後

讀完本篇 FlatBuffers 編碼原理以後,讀者應該能明白以下幾點:

與 protocol buffers 相比,FlatBuffers 的資料結構定義檔案,功能上有以下一些“改進”:

  • 棄用的欄位,不用手動分配欄位的 ID。在 .proto 中擴充套件一個物件,需要在數字中尋找一個空閒的空位(因為 protocol buffers 有更緊湊的表示方式,所以必須選擇更小的數字)。除了這點不方便之外,它還使得刪除欄位成為問題:如果保留它們,從語意表達上不是很明顯的表達出這個欄位不能讀寫了,保留它們,還會生成訪問器。如果刪除它們,就會有出現嚴重 bug 的風險,因為當有人重用了這些 ID,會導致讀取到舊的資料,這樣資料會發生錯亂。
  • FlatBuffers 區分 table 和 struct。所有 table 欄位都是可選的,並且所有 struct 欄位都是必需的。
  • FlatBuffers 具有原生陣列型別而不是 repeated。這給你一個長度,而不必收集所有專案,並且在標量的情況下提供更緊湊的表示,並且確保相鄰性。
  • FlatBuffers 具有 union 型別,這個也是 protocol buffers 沒有的。一個 union 可以替代很多個 optional 欄位,這樣也可以節約每個欄位都要一一檢查的時間。
  • FlatBuffers 能夠為所有標量定義預設值,而不必在每次訪問時處理它們的 optional,並且預設值不存在 buffer 中,也不用擔心空間的問題。
  • 可以統一處理模式和資料定義(並且和 JSON 相容)的解析器。protocol buffers 不相容 JSON。FlatBuffers 的 flatc 編譯器可帶的引數也更加強大,具體可帶引數列表見此文件
  • schema 擴充套件了一些 protocol buffers 沒有的 Attributes。

除去功能上的不同,再就是一些 schema 語法上的細微不同:

  • 定義物件,protocol buffers 是 message,FlatBuffers 是 table
  • ID,protocol buffers 預設是從 1 開始標號,FlatBuffers 預設從 0 開始。

關於 schema 所有的語法,可以參考這個文件

關於 flatbuffers 編解碼效能相關的,原理分析和原始碼分析,將在下篇進行。


Reference:

flatbuffers 官方文件
Improving Facebook's performance on Android with FlatBuffers

GitHub Repo:Halfrost-Field

Follow: halfrost · GitHub

Source: halfrost.com/flatbuffers…

相關文章