Android FlatBuffers實戰

xiangzhihong發表於2019-03-01

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耗時測試

參考:在Android中使用FlatBuffers
FlatBuffers官方doc

相關文章