在Android中使用比JSON性能高的FlatBuffers

FlatBuffers是google最新针对游戏开发退出的高性能的跨平台序列化工具,目前已经支持C++, C#, Go, Java, JavaScript, PHP, and Python (C和Ruby正在支持中),相对于json和Protocol Buffers,FlatBuffers在序列化和反序列化方面表现更为优异,而且需要的资源更少,更适合大部分移动应用的使用场景。
除了高性能和低内存消耗的特点,FlatBuffers还有其他一些优势,官方的总结说明如下:

  • 不用解析也能对数据进行访问
    FlatBuffers使用二进制结构化的方法来存储数据,可以不用先对整段数据进行解析就可以直接在二进制结构中访问到指定成员的信息。并且FlatBuffers还支持存储数据结构的前后兼容。
  • 内存占用小,运行效率高
    FlatBuffers使用ByteBuffer进行数据存储,在FlatBuffers的序列化和反序列化过程中不会额外占用其他内存空间,FlatBuffers读取数据的速度和直接从内存读取原始数据相当,仅仅多了一个相对寻址的耗时。同时,数据存储在ByteBuffer中还能够比较容易地支持内存映射(mmap)和流式读写,进一步降低对内存的消耗。
  • 灵活
    FlatBuffers支持选择性地写入数据成员,这不仅为某一个数据结构在应用的不同版本之间提供了兼容性,同时还能使程序员灵活地选择是否写入某些字段及灵活地设计传输的数据结构。
  • 体积小,集成成本低
    项目中集成FlatBuffers只需要很少的额外代码。
  • 强类型约束
    如果数据结构不符合FlatBufferss文件的描述,那么会在编译期间就发现问题,而不会在运行时才暴露问题。
  • 使用方便
    可以兼容json等其他格式的解析。
  • 跨平台

FlatBuffers和Protocol Buffers是比较相似的,但是FlatBuffers不需要在读取成员变量之前必须将数据完全解析成对象,因为它所有信息的读取都是在对应的ByteBuffer中进行的,少了这些解析时必须为对象和成员变量分配的内存空间,就降低了解析过程中的内存消耗。json相对于FlatBuffers来说可读性更好,但是缺点也是明显的,那就是它的性能太低了。JSON作为数据交换格式,被广泛用户各种动态语言之间(当然也包括静态语言)。它的优点是易于理解(可读性好),同时它的最大的缺点那就是解析时的性能问题了。而且因为它的动态类型特点,你的代码可能还需要多写好多类型、数据检查逻辑。

下文简介FBS的使用
FlatBuffers使用项目的整体流程:
在Android中使用比JSON性能高的FlatBuffers_第1张图片
在使用 FlatBuffers 时,我们需要以特殊的格式定义我们的结构化数据,保存为 .fbs 文件。 FlatBuffers 项目为我们提供了编译器,可用于将 .fbs 文件编译为Java文件,C++文件等,以用于我们的项目。 FlatBuffers 编译器在我们的开发机,比如Ubuntu,Mac上运行。这些源代码文件是基于 FlatBuffers 提供的Java库生成的,同时我们也需要利用这个Java库的一些接口来序列化或解析数据。
我们将 FlatBuffers 编译器生成的Java文件及 FlatBuffers 的Java库导入我们的项目,就可以用 FlatBuffers 来对我们的结构化数据执行序列化和反序列化了。

下载、编译FlatBuffers编译器

在https://github.com/google/flatbuffers/releases 获取官方发布的打包好的版本:

$ git clone https://github.com/google/flatbuffers.git
Cloning into 'flatbuffers'...
remote: Counting objects: 7340, done.
remote: Compressing objects: 100% (46/46), done.
remote: Total 7340 (delta 16), reused 0 (delta 0), pack-reused 7290
Receiving objects: 100% (7340/7340), 3.64 MiB | 115.00 KiB/s, done.
Resolving deltas: 100% (4692/4692), done.
Checking connectivity... done.

下载代码之后,我们需要用cmake工具来为flatbuffers生成Makefile文件并编译:

$ cd flatbuffers/
$ cmake CMakeLists.txt 
-- The C compiler identification is AppleClang 7.3.0.7030031
-- The CXX compiler identification is AppleClang 7.3.0.7030031
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/netease/Projects/OpenSource/flatbuffers
$ make && make install

安装之后执行如下命令以确认已经装好:

$ flatc --version
flatc version 1.4.0 (Dec  7 2016)

flatc没有为我们提供 –help 选项,不过加了错误的参数时这个工具会为我们展示详细的用法:

$ flatc --help
flatc: unknown commandline argument: --help
usage: flatc [OPTION]... FILE... [-- FILE...]
  --binary     -b Generate wire format binaries for any data definitions.
  --json       -t Generate text output for any data definitions.
  --cpp        -c Generate C++ headers for tables/structs.
  --go         -g Generate Go files for tables/structs.
  --java       -j Generate Java classes for tables/structs.
  --js         -s Generate JavaScript code for tables/structs.
  --csharp     -n Generate C# classes for tables/structs.
  --python     -p Generate Python files for tables/structs.
  --php           Generate PHP files for tables/structs.
  -o PATH            Prefix PATH to all generated files.
  -I PATH            Search for includes in the specified path.
  -M                 Print make rules for generated files.
  --version          Print the version number of flatc and exit.
  --strict-json      Strict JSON: field names must be / will be quoted,
                     no trailing commas in tables/vectors.
  --allow-non-utf8   Pass non-UTF-8 input through parser and emit nonstandard
                     \x escapes in JSON. (Default is to raise parse error on
                     non-UTF-8 input.)
  --defaults-json    Output fields whose value is the default when
                     writing JSON
  --unknown-json     Allow fields in JSON that are not defined in the
                     schema. These fields will be discared when generating
                     binaries.
  --no-prefix        Don't prefix enum values with the enum type in C++.
  --scoped-enums     Use C++11 style scoped and strongly typed enums.
                     also implies --no-prefix.
  --gen-includes     (deprecated), this is the default behavior.
                     If the original behavior is required (no include
                     statements) use --no-includes.
  --no-includes      Don't generate include statements for included
                     schemas the generated file depends on (C++).
  --gen-mutable      Generate accessors that can mutate buffers in-place.
  --gen-onefile      Generate single output file for C#.
  --gen-name-strings Generate type name functions for C++.
  --escape-proto-ids Disable appending '_' in namespaces names.
  --gen-object-api   Generate an additional object-based API.
  --cpp-ptr-type T   Set object API pointer type (default std::unique_ptr)
  --raw-binary       Allow binaries without file_indentifier to be read.
                     This may crash flatc given a mismatched schema.
  --proto            Input is a .proto, translate to .fbs.
  --schema           Serialize schemas instead of JSON (use with -b)
  --conform FILE     Specify a schema the following schemas should be
                     an evolution of. Gives errors if not.
  --conform-includes Include path for the schema given with --conform
    PATH             
FILEs may be schemas, or JSON files (conforming to preceding schema)
FILEs after the -- must be binary flatbuffer format files.
Output files are named using the base file name of the input,
and written to the current directory or the path given by -o.
example: flatc -c -b schema1.fbs schema2.fbs data.json

创建 .fbs 文件

flatc支持将为 Protocol Buffers 编写的 .proto 文件转换为 .fbs 文件,如:

$ ls
addressbook.proto
$ flatc --proto addressbook.proto 
$ ls -l
total 16
-rw-r--r--  1 netease  staff  431 12  7 17:21 addressbook.fbs
-rw-r--r--@ 1 netease  staff  486 12  1 15:18 addressbook.proto

Protocol Buffers消息文件中的一些写法, FlatBuffers 编译器还不能很好的支持,如option java_package,option java_outer_classname,和嵌套类。这里我们基于 FlatBuffers 编译器转换的 .proto 文件来获得我们的 .fbs 文件:

// Generated from addressbook.proto

namespace com.example.tutorial;

enum PhoneType : int {
  MOBILE = 0,
  HOME = 1,
  WORK = 2,
}

namespace com.example.tutorial;

table Person {
  name:string (required);
  id:int;
  email:string;
  phone:[com.example.tutorial._Person.PhoneNumber];
}

namespace com.example.tutorial._Person;

table PhoneNumber {
  number:string (required);
  type:int;
}

namespace com.example.tutorial;

table AddressBook {
  person:[com.example.tutorial.Person];
}

root_type AddressBook;

编译.fbs文件

可以通过如下命令编译 .fbs 文件:

$ flatc --java -o out addressbook.fbs

–java用于指定编译的目标编程语言。-o 参数则用于指定输出文件的路径,如过没有提供则将当前目录用作输出目录。 FlatBuffers 编译器按照为不同的数据结构声明的namespace生成目录结构。对于上面的例子,会生成如下的这些文件:

$ find out
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Menlo}span.s1 {font-variant-ligatures: no-common-ligatures}

$ find out/
out/
out//com
out//com/example
out//com/example/tutorial
out//com/example/tutorial/_Person
out//com/example/tutorial/_Person/PhoneNumber.java
out//com/example/tutorial/AddressBook.java
out//com/example/tutorial/Person.java
out//com/example/tutorial/PhoneType.java

在Android项目中使用FlatBuffers

我们将前面由 .fbs 文件生成的Java文件拷贝到我们的项目中。我们前面提到的, FlatBuffers 的Java库比较薄,当前并没有发不到jcenter这样的maven仓库中,因而我们需要将这部分代码也拷贝到我们的额项目中。 FlatBuffers 的Java库在其repo仓库的 java 目录下。引入这些文件之后,我们的代码结构如下
在Android中使用比JSON性能高的FlatBuffers_第2张图片
添加访问 FlatBuffers 的类:

package com.netease.volleydemo;

import com.example.tutorial.AddressBook;
import com.example.tutorial.Person;
import com.example.tutorial._Person.PhoneNumber;
import com.google.flatbuffers.FlatBufferBuilder;

import java.nio.ByteBuffer;

/**
 * Created by hanpfei0306 on 16-12-5.
 */

public class AddressBookFlatBuffers {
    public static ByteBuffer encodeTest(String[] names) {
        FlatBufferBuilder builder = new FlatBufferBuilder(0);

        int[] personOffsets = new int[names.length];

        for (int i = 0; i < names.length; ++ i) {
            int name = builder.createString(names[i]);
            int email = builder.createString("[email protected]");

            int number1 = builder.createString("0157-23443276");
            int type1 = 1;
            int phoneNumber1 = PhoneNumber.createPhoneNumber(builder, number1, type1);

            int number2 = builder.createString("136183667387");
            int type2 = 0;
            int phoneNumber2 = PhoneNumber.createPhoneNumber(builder, number2, type2);

            int[] phoneNubers = new int[2];
            phoneNubers[0] = phoneNumber1;
            phoneNubers[1] = phoneNumber2;

            int phoneNumbersPos = Person.createPhoneVector(builder, phoneNubers);

            int person = Person.createPerson(builder, name, 13958235, email, phoneNumbersPos);

            personOffsets[i] = person;
        }
        int persons = AddressBook.createPersonVector(builder, personOffsets);

        AddressBook.startAddressBook(builder);
        AddressBook.addPerson(builder, persons);
        int eab = AddressBook.endAddressBook(builder);
        builder.finish(eab);
        ByteBuffer buf = builder.dataBuffer();

        return buf;
    }

    public static ByteBuffer encodeTest(String[] names, int times) {
        for (int i = 0; i < times - 1; ++ i) {
            encodeTest(names);
        }
        return encodeTest(names);
    }

    public static AddressBook decodeTest(ByteBuffer byteBuffer) {
        AddressBook addressBook = null;
        addressBook = AddressBook.getRootAsAddressBook(byteBuffer);
        return addressBook;
    }

    public static AddressBook decodeTest(ByteBuffer byteBuffer, int times) {
        AddressBook addressBook = null;
        for (int i = 0; i < times; ++ i) {
            addressBook = decodeTest(byteBuffer);
        }
        return addressBook;
    }
}

使用 flatbuf-gradle-plugin

我们有开发一个 FlatBuffers 的gradle插件,以方便开发 。这个插件的设计有参考Google的protobuf-gradle-plugin,功能与用法也与protobuf-gradle-plugin类似。在这个项目中,我们也有为 FlatBuffers 的Java库创建一个module。

参考:

1、http://www.jianshu.com/p/3bac6bc80db7
2、https://code.facebook.com/posts/872547912839369/improving-facebook-s-performance-on-android-with-flatbuffers/

你可能感兴趣的:(Android开发)