FlatBuffers簡介
FlatBuffers是Google開源的一個跨平臺的、高效的、提供了C++/Java介面的序列化工具庫,它是Google專門為遊戲開發或其他效能敏感的應用程式需求而建立。尤其適用移動,嵌入式平臺,這些平臺在記憶體大小及頻寬相比桌面系統都是受限的,而應用程式比如遊戲又有更高的效能要求。它將序列化資料儲存在快取中,這些資料既可以儲存在檔案中,又可以通過網路原樣傳輸,而不需要任何解析開銷。以下是專案地址:
程式碼託管主頁:github.com/google/flat…
專案介紹主頁:google.github.io/flatbuffers…
FlatBuffers優勢
相比傳統的JSON和Protocol Buffers等序列化工具,FlatBuffers具有如下的一些優點:
- 不需要解析/拆包就可以訪問序列化資料:FlatBuffers與其他庫不同之處就在於它使用二進位制緩衝檔案來表示層次資料,這樣它們就可以被直接訪問而不需解析與拆包,同時還支援資料結構進化(前進、後退相容性)。
- 記憶體高效速度快 :訪問資料時只需要訪問記憶體中的緩衝區。它不需要多餘的記憶體分配(至少在C++是這樣,其他語言中可能會有變動)。
FlatBuffers還適合配合 mmap或資料流使用,只需要緩衝區的一部分儲存在記憶體中。訪問時速度接近原結構訪問,只有一點延遲(一種虛擬函式表vtable),是為了允許格式升級以 及可選欄位。FlatBuffers適合那些花費了大量時間和空間(記憶體分配)來訪問和構建序列化資料的專案,比如遊戲以及其他對錶現敏感的應用。可以參考FlatBuffers基準。 - 靈活 :由於具有可選欄位,你不但有很強的升級和回退相容性(對於歷史悠久的遊戲尤其重要,不用為了每個版本升級所有資料),在選擇要儲存哪些資料以及設計資料結構時也很自由。
- 輕量的code footprint:FlatBuffers只需要很少量的生成程式碼,以及一個表示最小依賴的很小的標頭檔案,很容易整合。
- 強型別:當編譯時報錯時,不需要自己寫重複的容易出錯的執行時檢查,它可以自動生成有用的程式碼。
- 使用方便:生成的C++程式碼允許精簡訪問與構建程式碼,還有可選的用於實現圖表解析、類似JSON的執行時字串展示等功能的方法。(後者比JSON解析庫更快,記憶體效率更高)。
- 程式碼跨平臺且沒有依賴:C++程式碼可以執行在任何近代的gcc/clang和VS2010上,同時還有用於測試和範例的構建檔案(Android中.mk檔案,其他平臺是cmake檔案)。
VS Protocol Buffers和JSON
Protocol Buffers的確和FlatBuffers比較類似,但其主要區別在於FlatBuffers在訪問資料前不需要解析/拆包這一步,而且Protocol Buffers既沒有可選的文字匯入/匯出功能,也沒有Schemas語法特性(比如union)。
JSON是一種輕量級的資料交換格式,JSON 可以將 JavaScript 物件中表示的一組資料轉換為字串,然後就可以在函式之間輕鬆地傳遞這個字串,或者在非同步應用程式中將字串從 Web 客戶機傳遞給伺服器端程式。JSON和動態型別語言(如JavaScript)一起使用時非常方便。然而在靜態型別語言中序列化資料時,JSON不但具有執行效率低的明顯缺點,而且會讓你寫更多的程式碼來訪問資料。
FlatBuffers究竟有多提高
- 解析速度:解析一個20KB的JSON流需要35ms,超過了UI重新整理間隔也就是16.6ms。如果解析JSON的話,我們就在滑動時就會因為要從磁碟載入快取而導致掉幀(視覺上的卡頓)。
- 解析器初始化 :一個JSON解析器需要先構建欄位對映再進行解析,這會花100ms到200ms,很明顯的拖緩App啟動時間。
- 垃圾回收 在解析JSON時建立了很多小物件,在我們的試驗中,解析20kb的JSON流時,要分配大約100kb的瞬時儲存,對Java記憶體回收造成很大壓力。
FlatBuffers實戰
FlatBuffers運作流程
首先來看一下FlatBuffers專案為開發者提供了哪些內容,可以從官網下載原始碼,其目錄結構如下圖:
如果要將FlatBuffers 用到我們的專案中,又需要哪些流程呢?可以參考下面的流程圖:
FlatBuffers用法
就像Parcel和Serializable的序列化一樣,FlatBuffers的是使用方式上也比最傳統的JSON序列化要複雜的多。在實際上面開發中,為了降低開發的難度,提高開發效率,我們會將原始碼編譯成可植入的第三方庫。下面以Java環境為例,來介紹FlatBuffers的簡單使用方法。讀者可以到對應的maven倉庫下載。
現在,假如我們拿到的json檔案的格式是下面這樣的:
{
"repos": [
{
"id": 27149168,
"name": "acai",
"full_name": "google/acai",
"owner": {
"login": "google",
"id": 1342004,
...
"type": "Organization",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/google/acai",
"description": "Testing library for JUnit4 and Guice.",
...
"watchers": 21,
"default_branch": "master"
},
...
]
}
複製程式碼
注:可以通過下面的連結來獲取更完整的json物件
模式檔案
我們需要準備一個model檔案,它定義了我們想要序列化/反序列化 的資料結構,這個模式將被flatc用於建立Java模型以及從JSON到FlatBuffer二進位制檔案的轉換。
現在,我們所要做的所有事情就是建立3個表:ReposList,Repo和User,並定義root_type。例如:
table ReposList {
repos : [Repo];
}
table Repo {
id : long;
name : string;
full_name : string;
owner : User;
//...
labels_url : string (deprecated);
releases_url : string (deprecated);
}
table User {
login : string;
id : long;
avatar_url : string;
gravatar_id : string;
//...
site_admin : bool;
}
root_type ReposList;
複製程式碼
注:完整的模式檔案可以點選下面的連結來獲取
FlatBuffers檔案
接下來,我們所需要做的就是將repos_json.json轉換為FlatBuffers二進位制檔案,併產生Java模型,其可以以Java友好的方式表示我們的資料,下面是轉換的命令:
$ ./flatc -j -b repos_schema.fbs repos_json.json
複製程式碼
如果沒有任何報錯,將會生成如下4個檔案:
repos_json.bin (將被重新命名為repos_flat.bin)
Repos/Repo.java
Repos/ReposList.java
Repos/User.java
複製程式碼
測試
接下來,我們可以使用FlatBuffers提供的Java庫來處理在Java中直接處理這種資料格式,此處使用需要使用到 flatbuffers-java-1.2.0-SNAPSHOT.jar。
public class MainActivity extends AppCompatActivity {
@Bind(R.id.tvFlat)
TextView tvFlat;
@Bind(R.id.tvJson)
TextView tvJson;
private RawDataReader rawDataReader;
private ReposListJson reposListJson;
private ReposList reposListFlat;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
rawDataReader = new RawDataReader(this);
}
@OnClick(R.id.btnJson)
public void onJsonClick() {
rawDataReader.loadJsonString(R.raw.repos_json).subscribe(new SimpleObserver<String>() {
@Override
public void onNext(String reposStr) {
parseReposListJson(reposStr);
}
});
}
private void parseReposListJson(String reposStr) {
long startTime = System.currentTimeMillis();
reposListJson = new Gson().fromJson(reposStr, ReposListJson.class);
for (int i = 0; i < reposListJson.repos.size(); i++) {
RepoJson repo = reposListJson.repos.get(i);
Log.d("FlatBuffers", "Repo #" + i + ", id: " + repo.id);
}
long endTime = System.currentTimeMillis() - startTime;
tvJson.setText("Elements: " + reposListJson.repos.size() + ": load time: " + endTime + "ms");
}
@OnClick(R.id.btnFlatBuffers)
public void onFlatBuffersClick() {
rawDataReader.loadBytes(R.raw.repos_flat).subscribe(new SimpleObserver<byte[]>() {
@Override
public void onNext(byte[] bytes) {
loadFlatBuffer(bytes);
}
});
}
private void loadFlatBuffer(byte[] bytes) {
long startTime = System.currentTimeMillis();
ByteBuffer bb = ByteBuffer.wrap(bytes);
reposListFlat = frogermcs.io.flatbuffs.model.flat.ReposList.getRootAsReposList(bb);
for (int i = 0; i < reposListFlat.reposLength(); i++) {
Repo repos = reposListFlat.repos(i);
Log.d("FlatBuffers", "Repo #" + i + ", id: " + repos.id());
}
long endTime = System.currentTimeMillis() - startTime;
tvFlat.setText("Elements: " + reposListFlat.reposLength() + ": load time: " + endTime + "ms");
}
}
複製程式碼
在上面的示例程式碼中,有兩個方法是比較核心的,需要我們注意。
- parseReposListJson(String reposStr) :初始化Gson解析器並將json字串轉換為Java物件。
- loadFlatBuffer(byte[] bytes) 將bytes(即repos_flat.bin檔案)轉為Java物件。
耗時測試
下面我們來測試下FlatBuffers和傳統的json在資料解析上的耗時,我們以4mb的JSON檔案為例。
如圖,可以發現FlatBuffers花了1-5ms,JSON花了大約2000ms。並且FlatBuffers期間Android App中沒有GC,而在使用JSON時發生了很多次GC,測試的原始碼可以通過以下地址下載:FlatBuffers耗時測試