替代Protocol buffers 的FlatBuffers:高效利用内存的序列化函数库(Unity中测试)

来源:互联网 发布:类似yolo的软件 编辑:程序博客网 时间:2024/06/07 02:01

孙广东   2015.7.4



http://www.open-open.com/lib/view/open1441004786315.html


             虽然FB的反序列化超级快,但是数据大小还是蛮大的(可能的原因是不需要解析,直接访问binary buffer,所以不能对数据进行压缩),带来的传输或存储开销,可能会抵过一部分反序列化的时间。感觉上,FB的使用场景上还是很受限,在移动端可能是一个应用场景。


protocol buffer的作者后来又弄了一个Cap'n Proto   但是好像应用场景没有太火


http://google.github.io/flatbuffers/flatbuffers_support.html


               提供了对  Unity游戏引擎C# 支持!(本身 FlatBuffers 就是 移动平台移动开发 而出的!)


 

关于Unity的示例代码  必须要学习啊!!!!!!


http://exiin.com/blog/flatbuffers-for-unity-sample-code/


            FlatBuffers 是 Google为游戏和移动应用而开发的开源、跨平台的序列化库。支持 C++, Java, C#, Go, Python and JavaScript等语言。中国有5亿智能手机,其中低端设备占多数,在 CPU 和内存都受限的情况下,能否开发出高性能且低内存占用的 Android程序决定了你的应用的用户覆盖率和留存率。


 

Unity使用 FlatBuffers 作为存储格式移动版)


翻译: http://qiita.com/akerusoft/items/8c10f8a40fee722e6d1b

Unity提供了 PlayerPrefs作为本地的数据存储,  但是速度不是很快!


下面就是一个测试使用 FlatBuffers


Unity 5.3.3f1 版本


FlatBuffers  1.3.0版本


运行在 Android /IOS 测试


FlatBuffers序列化保存为二进制。


cocos2dx 游戏数据使用


还应用Facebook上。


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;


来自 <http://qiita.com/akerusoft/items/8c10f8a40fee722e6d1b>


首先是表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的尝试


官方github DL 。

http://blog.csdn.net/u010019717

在脚本文件中的序列化/反序列化


要反序列化demo  github.


在 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的案例


翻译自:     http://exiin.com/blog/flatbuffers-for-unity-sample-code/

1-第一步下载编译器"flatc"和"FlatBuffers.dll"


Flatc.exe      用于将Schema转换成 c#


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


https://github.com/google/flatbuffers/releases


首先你要下载最新的  flatc.exe  文件,


然后你进入从Github上下载的源代码:路径下的  "\net\FlatBuffers"  文件夹  并打开"FlatBuffers.csproj"


并编译 "FlatBuffers.dll",你需要放到Unity的项目中assets/plugins文件夹内放)。


下一步是创建一个单独的文件夹,     放着我的编译器 schema 文件

Captur52423e

这里我做了一个批处理脚本 ( 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"的新文件

Captu4545re

           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,然后下载此文件︰


FlatBuffersTest.zip


 


来自 <http://exiin.com/blog/flatbuffers-for-unity-sample-code/>


快速上手


接下来将会简要介绍如何使用FlatBuffers。


  • 首先,为你要进行序列化操作的数据结构编写一个schema文件。在定义数据结构的属性时,你可以使用基本类型(各种长度的整形与浮点型),也可以是一个字符串,一个任意类型的数组,一个自定义的对象,甚至是一个自定义对象的集合(unions)。属性的值都是允许为空的,同时也可以设置默认值,所以这样一来,每个构建的对象可以根据自己的需要设置属性值。
  • 然后,用flatc命令(FlatBuffer的编译器)去生成一个C++头文件(或者生成Java/C#/Go/Python等等其他语言的相关文件),进而通过相应的辅助类文件来构建序列化数据。这个生成的C++头文件(比如 mydata_generated.h)只依赖flatbuffers.h,顺便补充一句,flatbuffers.h里包含了很多核心的函数。
  • 接着,使用FlatBufferBuilder类去构建一个单层级的二进制流数据。通过之前flatc命令生成好的代码以及FlatBufferBuilder的使用,简单的一些函数调用,就可以让你很自如地向二进制流中加入对象。
  • 好了,是时候将生成的二进制数据存起来了!或者,你是在做网络通信,那就它这些数据传输出去吧!
  • 当然,从另一个角度来说,当你获取到二进制数据,要将他解析成对象的时候,你可以将数据指针指向它的根对象,然后你可以在需要的位置方便地将它进行转换,获取你要的属性(object->field())。


开发资源


  • 如何构建编译环境,如何在多平台上运行示例代码.


  • 如何使用编译器.
  • 如何编写一个schema
  • 如何将生成的C++代码加入到你自己程序中
  • 如何将生成的C#/Java代码加入到你自己程序中
  • 如何将生成的Co代码加入到你自己程序中


  • FlatBuffers对平台、语言、特性的支持度


  • Benchmarks数据——看看FlatBuffers的优势


  • FlatBuffers的白皮书
  • FlatBuffers内部结构介绍


  • schema正式语法


网络资源


  • GitHub 库
  • 官方主页
  • FlatBuffers Google 小组
  • FlatBuffers 相关讨论


  • 独立化实现的工具:FlatCC  另一个FlatBuffers的解析工具与代码生成器,运行在C上
  • 视频资源:
    • Colt’s DevByte.
    • GDC 2015 Lightning Talk.
    • FlatBuffers for Go.
    • Evolution of FlatBuffers visualization.
  • 其他同仁创作的好文档:
    • FlatBuffers in Go
    • FlatBuffers in Android
    • Parsing JSON to FlatBuffers in Java
    • FlatBuffers in Unity


测试案例

http://blog.csdn.net/u010019717

在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

http://blog.csdn.net/u010019717




1 0