1. 程式人生 > >替代Protocol buffers 的FlatBuffers:高效利用記憶體的序列化函式庫(Unity中測試)

替代Protocol buffers 的FlatBuffers:高效利用記憶體的序列化函式庫(Unity中測試)

孫廣東   2015.7.4

雖然FB的反序列化超級快,但是資料大小還是蠻大的(可能的原因是不需要解析,直接訪問binary buffer,所以不能對資料進行壓縮),帶來的傳輸或儲存開銷,可能會抵過一部分反序列化的時間。感覺上,FB的使用場景上還是很受限,在移動端可能是一個應用場景。

protocol buffer的作者後來又弄了一個Cap'n Proto但是好像應用場景沒有太火

提供了對Unity遊戲引擎C#支援!(本身 FlatBuffers 就是移動平臺移動開發而出的!)

關於Unity的示例程式碼必須要學習啊!!!!!!

FlatBuffers 是 Google為遊戲和移動應用而開發的開源、跨平臺的序列化庫。支援 C++, Java, C#, Go, Python and JavaScript等語言。中國有5億智慧手機,其中低端裝置佔多數,在 CPU 和記憶體都受限的情況下,能否開發出高效能且低記憶體佔用的 Android程式決定了你的應用的使用者覆蓋率和留存率。

Unity使用 FlatBuffers 作為儲存格式移動版)

Unity提供了 PlayerPrefs作為本地的資料儲存,但是速度不是很快!

下面就是一個測試使用FlatBuffers

Unity 5.3.3f1 版本

FlatBuffers1.3.0版本

執行在Android /IOS 測試

FlatBuffers序列化儲存為二進位制。

FlatBuffers,分為結構定義 (架構) 和 資料

它要在伺服器和客戶端可用。

使用 FlatBuffers 的流程

  • 結構定義
  • 自動生成的相應的語言(java,C# ,java等)檔案
  • 序列化/反序列化過程

schema language (aka IDL, Interface DefinitionLanguage)

namespace Data;

file_identifier"MYFI";

unionData
{
MonsterDataV1
}

tableRoot
{
data:Data;
}

tableMonsterDataV1
{
name:string;
hp:uint;
hitRate:float;
speed:uint;
luck:uint;
}

tableMonsterDataV2
{
name:string;
hp:uint;
hitRate:float;
speed:uint;
luck:uint;
defence:uint;
}

root_typeRoot;

首先是表table MonsterDataV1

假設是在 rpg 遊戲中使用的怪物

結構

table 欄位宣告(型別寫在後面)

欄位是通過使用內建型別進行描述。

除了內建型別,你也可以使用自定義的型別(除了當前table

以下是union的資料。

這是種列舉型別,主要特徵table表作為欄位。

在本例中是定義的欄位只有 MonsterDataV1

union中的所有欄位佔用同一個記憶體儲存(同一時間也只有一個欄位有效)。

如果你也可以新增 MonsterDataV2 欄位,但資料也只有一個儲存。(聽起來像一個 c 語言中union

基於這種分析。

"file_identifier"是寫在檔案的開頭的檔案 ID,您可以使用4 ASCII 字元 (位元組 4-7)。

它是可能要檢查伺服器傳送資料,等等......

升級注意事項

應用程式或遊戲,要進行資料升級,當一項新功能被增加應用程式

但是schema 有一定的侷限性。

  1. 應該不會刪除欄位,如果資料已經在使用
  2. 不更改宣告欄位的順序 (新資料在後面追加就行)

升級的Demo

小的是升級,將向表中新增欄位。

之前的MonsterDataV1宣告:

table MonsterDataV1
{
name:string;
hp:uint;
hitRate:float;
speed:uint;
luck:uint;
}

新增新的欄位:

table MonsterDataV1
{
name:string;
hp:uint;
hitRate:float;
speed:uint;
luck:uint; (deprecated)
hoge1:[ubyte];
hoge2:bool;
hoge3:int;
hoge4:long;
hoge5:long; (deprecated)
hoge6:long;
hoge7:long;
hoge8:long; (deprecated)
hoge9:long;
}

union中新增新的 table型別!!!!!

unionData
{
MonsterDataV1,
MonsterDataV2
}
程式的輸出

Flatc 可執行檔案程式的輸出。

Windows 版本是 exe 在github頁上的窗體。

對於一個 c# 檔案生成用下面的命令。

flatc.exe -o OUTPUT_PATH -n--gen-onefile inputfile.fbs

"OUTPUT_PATH"請設定路徑輸出。

要輸出一個 c# 檔案引數 '-n'

"--gen-onefile"生成 cs 檔案時將被輸出到一個單獨的檔案。

您宣告資料定義schema檔案的名稱是"inputfile.fbs"(所以schema對檔名和字尾名美沒有任何限制吧!)

或者直至這樣寫:flatc -n schemaTemplate.txt --gen-onefile

要匯入到Unity的執行時檔案

執行 FlatBuffers 專案將生成的Dll 匯入到Unity的嘗試

在指令碼檔案中的序列化/反序列化

要反序列化demogithub.

在 Test1 test2 序列化反序列化過程中的執行。

ButtonCreate.cs

stringpath = Path.Combine(Application.persistentDataPath, DataName.FILE_NAME);

if(File.Exists(path))
File.Delete(path);

FlatBufferBuilderbuilder = new FlatBufferBuilder(1);

intoffestData;
Data.Data dataType;

Offset<Data.MonsterDataV1>data = Data.MonsterDataV1.CreateMonsterDataV1(builder,builder.CreateString(name), hp, hitRate, speed, luck);
offestData = data.Value;
dataType = Data.Data.MonsterDataV1;

Data.Root.StartRoot(builder);
Data.Root.AddDataType(builder,dataType);
Data.Root.AddData(builder,offestData);
Offset<Data.Root> endOffset =Data.Root.EndRoot(builder);

Data.Root.FinishRootBuffer(builder,endOffset);

bytes =builder.SizedByteArray();

File.WriteAllBytes(path,bytes);

反序列化過程:

ButtonLoad.cs

stringpath = Path.Combine(Application.persistentDataPath, DataName.FILE_NAME);

ByteBufferbuffer = new ByteBuffer(File.ReadAllBytes(path));

Data.Rootroot = Data.Root.GetRootAsRoot(buffer);

Data.DatadataType = root.DataType;

switch(dataType)
{
caseData.Data.MonsterDataV1:
Data.MonsterDataV1 monsterV1= root.GetData<Data.MonsterDataV1>(new Data.MonsterDataV1());

if(monsterV1 == null)
{
Debug.LogError("Failed load monster data version1.");
return;
}

textVersion.text= "Version1";

if(Encoding.Default != Encoding.UTF8)
textName.text =Encoding.Default.GetString(Encoding.Convert(Encoding.UTF8, Encoding.Default,Encoding.UTF8.GetBytes(monsterV1.Name)));
else
textName.text =monsterV1.Name;
textHp.text =monsterV1.Hp.ToString();
textHitRate.text =monsterV1.HitRate.ToString();
textSpeed.text =monsterV1.Speed.ToString();
textLuck.text =monsterV1.Luck.ToString();
textDefence.text = "Nodata";
break;
}

每種型別的反序列化過程中的過濾器在union中定義的型別。

這意味著的訊息傳遞時你重置資料定義,應該用來讀取資料。

測試程式碼在 github 上的有一些不同。

github 上的程式碼執行加密過程。

(加密 / 解密處理,處理時間會消耗更長的時間......)

FlatBuffers 的思想

cocos2dx spine

json 輸出資料是spine的載入很慢。

以二進位制格式是相當棒的

嘗試使用及時研究的利與弊??

Unity中使用 FlatBuffers的案例

1-第一步下載編譯器"flatc"和"FlatBuffers.dll"

Flatc.exe用於將Schema轉換成 c#

您可以下載最新的版本FlatBuffers.dll

首先你要下載最新的flatc.exe檔案,

然後你進入從Github上下載的原始碼:路徑下的"\net\FlatBuffers"資料夾並開啟"FlatBuffers.csproj"

並編譯"FlatBuffers.dll",你需要放到Unity的專案中assets/plugins資料夾內放)。

下一步是建立一個單獨的資料夾,放著我的編譯器schema 檔案

這裡我做了一個批處理指令碼 ( compile.bat ) 包含這些行︰

flatc -n SaveSchema.txt--gen-onefile @pause

1

2

flatc -n SaveSchema.txt --gen-onefile

@pause

用於演示,我將使用一個 SaveSchema.txt 檔案︰

//example save file

namespaceCompanyNamespaceWhatever;

enumColor : byte { Red = 1, Green, Blue }

unionWeaponClassesOrWhatever { Sword, Gun }

structVec3 {

x:float;

y:float;

z:float;

}

tableGameDataWhatever {

pos:Vec3;

mana:short = 150;

hp:short = 100;

name:string;

inventory:[ubyte];

color:Color = Blue;

weapon:WeaponClassesOrWhatever;

}

tableSword {

damage:int = 10;

distance:short = 5;

}

tableGun {

damage:int = 500;

reloadspeed:short = 2;

}

root_typeGameDataWhatever;

file_identifier"WHAT";

schema 檔案定義了要儲存的資料的結構體

關於schema 語法的更多資訊請閱讀這個官方文件頁面.

一旦你執行compile.bat,它會建立一個名為"SavedSchema.cs"的新檔案

Flatc現在生成的幾個類的名稱,如"WeaponClassesOrWhatever"

這個C#檔案是你整個系統的載入和儲存schema的作用

2-下一步,我們如何儲存我們的資料?

生成的.cs檔案包含的所有類和需儲存和載入資料。

到您的專案生成檔案的地方

(也請不要忘記將"FlatBuffers.dll"放到您的專案,否則你會看到一些錯誤)

然後將此程式碼︰

// Create flatbufferclass

FlatBufferBuilderfbb = new FlatBufferBuilder(1);

// Create our swordfor GameDataWhatever

//------------------------------------------------------

WeaponClassesOrWhateverweaponType = WeaponClassesOrWhatever.Sword;

Sword.StartSword(fbb);

Sword.AddDamage(fbb,123);

Sword.AddDistance(fbb,999);

Offset<Sword>offsetWeapon = Sword.EndSword(fbb);

/*

// For gun uncommentthis one and remove the sword one

WeaponClassesOrWhateverweaponType = WeaponClassesOrWhatever.Gun;

Gun.StartGun(fbb);

Gun.AddDamage(fbb,123);

Gun.AddReloadspeed(fbb,999);

Offset<Gun>offsetWeapon = Gun.EndGun(fbb);

*/

//------------------------------------------------------

// Create stringsfor GameDataWhatever

//------------------------------------------------------

StringOffset cname =fbb.CreateString("Test String ! time : " + DateTime.Now);

//------------------------------------------------------

// CreateGameDataWhatever object we will store string and weapon in

//------------------------------------------------------

GameDataWhatever.StartGameDataWhatever(fbb);

GameDataWhatever.AddName(fbb,cname);

GameDataWhatever.AddPos(fbb,Vec3.CreateVec3(fbb, 1, 2, 1)); // structs can be inserted directly, no need tobe defined earlier

GameDataWhatever.AddColor(fbb,CompanyNamespaceWhatever.Color.Red);

//Store weapon

GameDataWhatever.AddWeaponType(fbb,weaponType);

GameDataWhatever.AddWeapon(fbb,offsetWeapon.Value);

var offset =GameDataWhatever.EndGameDataWhatever(fbb);

//------------------------------------------------------

GameDataWhatever.FinishGameDataWhateverBuffer(fbb,offset);

// Save the datainto "SAVE_FILENAME.whatever" file, name doesn't matter obviously

using (var ms = newMemoryStream(fbb.DataBuffer.Data, fbb.DataBuffer.Position, fbb.Offset)) {

File.WriteAllBytes("SAVE_FILENAME.whatever", ms.ToArray());

Debug.Log("SAVED !");

}

你寫你的資料的方式是順序依賴.

你總是要由內而外建立專案.

從一切開始該物件包含(如字串、 陣列、 其他物件) 的物件本身。

基本上,如果使用了其他的物件,就要預先設定其他的物件!

所以這裡發生的是︰

我們要先建立的weapon string ,因為GameDataWhatever裡面使用了他們

儲蓄的一部分可以真的很棘手,我勸你讀這篇文章有更好的理解如何儲存資料。

3-最後,讀取檔案是小菜一碟。

可以按任何順序,如果你想要你甚至不需要去通過的所有值,因為flatbuffers 工作像變魔術一樣 !

ByteBuffer bb = newByteBuffer(File.ReadAllBytes("SAVE_FILENAME.whatever"));

if(!GameDataWhatever.GameDataWhateverBufferHasIdentifier(bb)) {

throw new Exception("Identifier testfailed, you sure the identifier is identical to the generated schema'sone?");

}

GameDataWhateverdata = GameDataWhatever.GetRootAsGameDataWhatever(bb);

Debug.Log("LOADEDDATA : ");

Debug.Log("NAME: " + data.Name);

Debug.Log("POS: " + data.Pos.X + ", " + data.Pos.Y + ", " +data.Pos.Z);

Debug.Log("COLOR: " + data.Color);

Debug.Log("WEAPONTYPE : " + data.WeaponType);

switch(data.WeaponType) {

case WeaponClassesOrWhatever.Sword:

Sword sword = new Sword();

data.GetWeapon<Sword>(sword);

Debug.Log("SWORD DAMAGE: " + sword.Damage);

break;

case WeaponClassesOrWhatever.Gun:

Gun gun = new Gun();

data.GetWeapon<Gun>(gun);

Debug.Log("GUN RELOAD SPEED: " + gun.Reloadspeed);

break;

default:

break;

}

我們測試了flatbuffers 對所有主要的移動平臺 (iOS,Android,亞馬遜的作業系統,Windows Phone) 它工作得很好。

4 Unity中的原始碼

如果你想示例程式碼和已編譯的flatbuffer 檔案 flatc.exe 和 flatbuffers.dll,然後下載此檔案︰

快速上手

接下來將會簡要介紹如何使用FlatBuffers。

  • 首先,為你要進行序列化操作的資料結構編寫一個schema檔案。在定義資料結構的屬性時,你可以使用基本型別(各種長度的整形與浮點型),也可以是一個字串,一個任意型別的陣列,一個自定義的物件,甚至是一個自定義物件的集合(unions)。屬性的值都是允許為空的,同時也可以設定預設值,所以這樣一來,每個構建的物件可以根據自己的需要設定屬性值。
  • 然後,用flatc命令(FlatBuffer的編譯器)去生成一個C++標頭檔案(或者生成Java/C#/Go/Python等等其他語言的相關檔案),進而通過相應的輔助類檔案來構建序列化資料。這個生成的C++標頭檔案(比如 mydata_generated.h)只依賴flatbuffers.h,順便補充一句,flatbuffers.h裡包含了很多核心的函式。
  • 接著,使用FlatBufferBuilder類去構建一個單層級的二進位制流資料。通過之前flatc命令生成好的程式碼以及FlatBufferBuilder的使用,簡單的一些函式呼叫,就可以讓你很自如地向二進位制流中加入物件。
  • 好了,是時候將生成的二進位制資料存起來了!或者,你是在做網路通訊,那就它這些資料傳輸出去吧!
  • 當然,從另一個角度來說,當你獲取到二進位制資料,要將他解析成物件的時候,你可以將資料指標指向它的根物件,然後你可以在需要的位置方便地將它進行轉換,獲取你要的屬性(object->field())。

開發資源

網路資源

測試案例

在Google的Benchmark中,已經明確表明了其效能優勢,槓槓的啊!

考慮到其擴語言與輕量級的特性,筆者專門自己做了一些較為貼近生產場景的測試,對比的是開發常用的1.1.41版本的fastjson

貼上程式碼

FlatBuffer與fastjson的比較

packageflatbuffers.test;

importjava.nio.ByteBuffer;

importjava.util.Arrays;

importcom.alibaba.fastjson.JSONObject;

importflatbuffers.FlatBufferBuilder;

importflatbuffers.schema.Product;

public classBufferCompareTest {

static final long LOOP_COUNT = 5000000;

public static void main(String[] args) {

System.out.println("執行"+LOOP_COUNT+"次解析耗時比較");

//FlatBuffers

byte[] dataByte =buildFlatBuffersByte();

long startFlatBuffers =System.currentTimeMillis();

Product p = null;

for (int i = 0; i < LOOP_COUNT; i++){

p =Product.getRootAsProduct(ByteBuffer.wrap(dataByte));

}

System.out.println("FlatBuffers :" + (System.currentTimeMillis() - startFlatBuffers)+"ms");

//JSON

String jsonStr ="{\"marketable\":\"true\",\"costPrice\":15000,\"imgUrl\":\"http://img2.bbgstatic.com/14e2a07cbd5_2_8f02bdb34427ec107124fd1576287310_800x800.jpeg\",\"productBn\":\"MG120394938912345\",\"productId\":123456,\"productName\":\"德國愛他美Aptamil Pre 0-3個月 日期至167-8月左右\",\"salePrice\":15500,\"shopId\":123,\"shopName\":\"雲猴全球購\"}";

long startJSON =System.currentTimeMillis();

JSONObject obj = null;

for (int i = 0; i < LOOP_COUNT; i++){

obj =JSONObject.parseObject(jsonStr);

}

System.out.println("JSON : "+ (System.currentTimeMillis() - startJSON)+"ms");

}

private static byte[]buildFlatBuffersByte() {

FlatBufferBuilder fbb = newFlatBufferBuilder(1);

int productBnOS =fbb.createString("MG120394938912345");

int productNameOS =fbb.createString("德國愛他美Aptamil Pre 0-3個月 日期至167-8月左右");

int shopNameOS =fbb.createString("雲猴全球購");

int imgUrlOS =fbb.createString("http://img2.bbgstatic.com/14e2a07cbd5_2_8f02bdb34427ec107124fd1576287310_800x800.jpeg");

//注意,建立字串(createString)要在XXX.startXXX方法執行之前,否則會出現異常

Product.startProduct(fbb);

Product.addProductId(fbb, 123456l);

Product.addShopId(fbb, 123);

Product.addProductBn(fbb, productBnOS);

Product.addProductName(fbb,productNameOS);

Product.addShopName(fbb, shopNameOS);

Product.addImgUrl(fbb, imgUrlOS);

Product.addCostPrice(fbb, 15000l);

Product.addSalePrice(fbb, 15500l);

Product.addMarketable(fbb, true);

int endOffset =Product.endProduct(fbb);

Product.finishProductBuffer(fbb,endOffset);

byte[] originalData =fbb.dataBuffer().array();

byte[] dataByte =Arrays.copyOfRange(originalData, fbb.dataBuffer().position(),(fbb.dataBuffer().position() +fbb.offset()));

return dataByte;

}

}

結果是,還是有點差距的:

執行5000000次解析耗時比較

FlatBuffers : 98ms

JSON : 10375ms