替代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 有一定的侷限性。
- 應該不會刪除欄位,如果資料已經在使用,
- 不更改宣告欄位的順序 (新資料在後面追加就行)
升級的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())。
開發資源
- 如何構建編譯環境,如何在多平臺上執行示例程式碼.
- Benchmarks資料——看看FlatBuffers的優勢
網路資源
測試案例
在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個月 日期至16年7-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個月 日期至16年7-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