客户端与服务端经常进行着频繁的数据传输,而数据传输又影响着用户体验,本文就传输速率的优化,提出合理的优化建议
传统的传输方案
在开始的时候,采用的是xml传输,这就要使用到Serializable/Parcelable
序列化以及反序列化,其传输速度之慢,基本已经被遗弃,后来又出现了JSON序列化传输,其常用工具就是GSON和fastjson,但随着时代的进步,json也体现出了局限性
json的局限性主要体现在其是基于字符串的传输,在转换的时候会生成大量JsonObject,然后转化为字符串,送进流里面,然后传输,在服务端也要从流中取出,然后反序列化,一大堆繁琐的过程,其也渐渐不适合当今传输数据的要求
那么什么样的方案才满足当今数据传输的要求呢?
新的数据传输方式
现在有如下选择可以用
Protocal Buffers
:强大,灵活,但是对内存的消耗会比较大,并不是移动终端上的最佳选择Nano-Proto-Buffers
:基于Protocal,为移动终端做了特殊的优化,代码执行效率更高,内存使用效率更佳FlatBuffers
:这个开源库最开始是由Google研发的,专注于提供更优秀的性能
以下两幅图是这三个工具的性能对比
可见,FlatBuffers
几乎从空间和时间复杂度上完胜其他技术
FlatBuffers
是一个开源的跨平台数据序列化库,可以应用到几乎任何语言(C++
,C#
,Go
,Java
,JavaScript
,PHP
,Python
),最开始是Google
为游戏或者其他对性能要求很高的应用开发的。项目地址在GitHub上。官方的文档在这里
FlatBuffer
的优点
FlatBuffer
相对于其他序列化技术,例如XML
,JSON
,Protocol Buffers
等,有哪些优势呢?官方文档的说法如下:
- 直接读取序列化数据,而不需要解析(
Parsing
)或者解包(Unpacking
):FlatBuffer
把数据层级结构保存在一个扁平化的二进制缓存(一维数组)中,同时能够保持直接获取里面的结构化数据,而不需要解析,并且还能保证数据结构变化的前后向兼容 - 高效的内存使用和速度:FlatBuffer 使用过程中,不需要额外的内存,几乎接近原始数据在内存中的大小
- 灵活:数据能够前后向兼容,并且能够灵活控制你的数据结构
- 很少的代码侵入性:使用少量的自动生成的代码即可实现
- 强数据类性,易于使用,跨平台,几乎语言无关
JSON是Android中很常用的数据序列化技术,但却很消耗内存,而FlatBuffer
正好解决了这个问题,性能还更好了
使用方法
简单来说:FlatBuffers
的使用方法是,首先按照使用特定的IDL
定义数据结构schema
,然后使用编译工具flatc
编译schema
生成对应的代码,把生成的代码应用到工程中即可
- 首先,我们需要得到
flatc
,这个需要从源码编辑得到。从GitHub
上Clone
代码
git clone https://github.com/google/flatbuffers
首先要使用FlatBuffers
的IDL
定义好数据结构Schema
,编写Schema
的详细文档在这里。其语法和C
语言类似,比较容易上手。我们这里引用一个简单的例子,假设数据结构如下:
class Person {
String name;
int friendshipStatus;
Person spouse;
List<Person>friends;
}
编写成Schema
如下,文件名为Person.fbs
:
namespace com.race604.fbs;
enum FriendshipStatus: int {Friend = 1, NotFriend}
table Person {
name: string;
friendshipStatus: FriendshipStatus = Friend;
spouse: Person;
friends: [Person];
}
root_type Person;
然后,使用flatc
可以把Schema
编译成多种编程语言,我们仅仅讨论Android
平台,所以把Schema
编译成Java
,找到flatc.exe
执行命令如下:
./flatc –j -b Person.fbs
在当前目录生成如下文件:
.
└── com
└── race604
└── fbs
├── FriendshipStatus.java
└── Person.java
Person
类有响应的函数直接获取其内部的属性值,使用非常简单:
Person person = ...;
// 获取普通成员
String name = person.name();
int friendshipStatus = person.friendshipStatus();
// 获取数组
int length = person.friendsLength()
for (int i = 0; i < length; i++) {
Person friends = person.friends(i);
...
}
下面我们来构建一个Person
对象,名字是"John"
,其配偶(spouse)是"Mary"
,还有两个朋友,分别是"Dave"
和"Tom"
,实现如下:
private ByteBuffer createPerson() {
FlatBufferBuilder builder = new FlatBufferBuilder(0);
int spouseName = builder.createString("Mary");
int spouse = Person.createPerson(builder, spouseName, FriendshipStatus.Friend, 0, 0);
int friendDave = Person.createPerson(builder, builder.createString("Dave"),
FriendshipStatus.Friend, 0, 0);
int friendTom = Person.createPerson(builder, builder.createString("Tom"),
FriendshipStatus.Friend, 0, 0);
int name = builder.createString("John");
int[] friendsArr = new int[]{ friendDave, friendTom };
int friends = Person.createFriendsVector(builder, friendsArr);
Person.startPerson(builder);
Person.addName(builder, name);
Person.addSpouse(builder, spouse);
Person.addFriends(builder, friends);
Person.addFriendshipStatus(builder, FriendshipStatus.NotFriend);
int john = Person.endPerson(builder);
builder.finish(john);
return builder.dataBuffer();
}
基本方法就是通过FlatBufferBuilder
工具,往里面填写数据,详细的写法可以参考官方文档。可见,其实写法略显繁琐,不太直观
基本原理
如官方文档的介绍,FlatBuffers
就像它的名字所表示的一样,就是把结构化的对象,用一个扁平化(Flat)的缓冲区保存,简单的来说就是把内存对象数据,保存在一个一维的数组中。借用Facebook
文章的一张图如下:
可见,FlatBuffers
保存在一个byte
数组中,有一个支点
指针(pivot point
)以此为界,存储的内容分为两个部分:元数据和数据内容。其中元数据部分就是数据在前面,其长度等于对象中的字段数量,每个byte
保存对应字段内容在数组中的索引(从支点位置开始计算)
如图,上面的Person
对象第一个字段是name
,其值的索引位置是1
,所以从索引位置1
开始的字符串,就是name
字段的值"John"
。第二个字段是friendshipStatus
,其索引值是6
,找到值为2
, 表示NotFriend
。第三个字段是spouse
,也一个Person
对象,索引值是12
,指向的是此对象的支点位置。第四个字段是一个数组,图中表示的数组为空,所以索引值是0
通过上面的解析,可以看出,FlatBuffers
通过自己分配和管理对象的存储,使对象在内存中就是线性结构化的,直接可以把内存内容保存或者发送出去,加载解析
数据只需要把byte
数组加载到内存中即可,不需要任何解析,也不产生任何中间变量
它与具体的机器或者运行环境无关,例如在Java
中,对象内的内存不依赖Java
虚拟机的堆内存分配策略实现,所以也是跨平台的
使用建议
通过前面的体验,FlatBuffers
几乎秒杀了JSON
下面说说FlatBuffers
的几点缺点:
FlatBuffers
需要生成代码,对代码有侵入性- 数据序列化没有可读性,不方便 Debug
- 构建
FlatBuffers
对象比较麻烦,不直观,特别是如果对象比较复杂情况下需要写大段的代码 - 数据的所有内容需要使用
Schema
严格定义,灵活性不如JSON
所以,在什么情况下选择使用FlatBuffers
呢?个人感觉需要满足以下几点:
1.项目中有大量数据传输和解析,使用JSON
成为了性能瓶颈
2.稳定的数据结构定义
用一个完整例子说明
假如存在一个数据结构Items,Items里面有很多属性,其中Items又包含LetterItems,LetterItems又有自己的属性还包含Details,其又有自己的属性,那么这样一个结构应该怎样去写成fbs
文件呢?编写文本文件,其后缀名要为.fbs
namespace com.cj5785.flatbufferstest;
table Items {
id:int;
title:string;
show:bool;
time:long;
LetterItems:[LetterItems];
}
table LetterItems {
id:int;
title:string;
author:string;
time:long;
Details:[Details];
}
table Details {
id:int;
name:string;
price:double;
date:long;
}
root_type Items;
fbs
还支持enum
、union
、struct
的定义
windows
平台可以直接下载flatc.exe
使用:https://github.com/google/flatbuffers/releases
使用如下命令生成文件
flatc --java test.fbs
执行上述命令会生成三个java文件:Items.java
、LetterItems.java
、Details.java
将生成的文件和FlatBufferBuilder.java
以及Table.java
复制到项目目录中
适当修改包名和一些引用错误,就完美融入到项目中了
接下来做一个简单测试,序列化然后写入本地,之后再读取出来显示出来
这里做了一个简单布局。一个TextView用来显示,两个Button,一个解析,一个读取,为了方便,直接使用了onClick属性
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "cj5785";
private TextView textView;
private String path;
private File file;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.text_view);
path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "flattest.bin";
file = new File(path);
}
public void serialize(View view) {
FlatBufferBuilder flatBufferBuilder = new FlatBufferBuilder();
long startTime = System.currentTimeMillis();
//Details数据
int orange = flatBufferBuilder.createString("orange");
int orangeDetail = Details.createDetails(flatBufferBuilder, 1, orange, 5.0, 20180101L);
int apple = flatBufferBuilder.createString("apple");
int appleDetail = Details.createDetails(flatBufferBuilder, 2, apple, 8.0, 20180101L);
int details[] = new int[2];
details[0] = orangeDetail;
details[1] = appleDetail;
int detailsList = LetterItems.createDetailsVector(flatBufferBuilder, details);
//LetterItems数据
int title = flatBufferBuilder.createString("title");
int author = flatBufferBuilder.createString("author");
int letterItems = LetterItems.createLetterItems(flatBufferBuilder, 1, title, author,
20180101L, detailsList);
int letterItemsList = Items.createLetterItemsVector(flatBufferBuilder, new int[]{letterItems});
//Items根数据
//在开始构建根的时候,不允许再创建,否则会报错:object serialization must not be nested
int titleOffset = flatBufferBuilder.createString("article");
Items.startItems(flatBufferBuilder);
Items.addId(flatBufferBuilder, 1);
Items.addTitle(flatBufferBuilder, titleOffset);
Items.addShow(flatBufferBuilder, false);
Items.addTime(flatBufferBuilder, 20180101L);
Items.addLetterItems(flatBufferBuilder, letterItemsList);
int rootItems = Items.endItems(flatBufferBuilder);
Items.finishItemsBuffer(flatBufferBuilder, rootItems);
long endTime = System.currentTimeMillis();
textView.setText("序列化用时:" + (endTime - startTime) + "ms
");
textView.append("写入的数据为:
");
textView.append("Item(1,article,false,20180101L,*)
");
textView.append("LetterItems(1,title,author,20180101L,*)
");
textView.append("Details(1,organge,5.0,20180101L)
");
textView.append("Details(2,apple,5.0,20180101L)
");
//保存文件到本地
if (file.exists()) {
file.delete();
}
ByteBuffer data = flatBufferBuilder.dataBuffer();
FileOutputStream out = null;
FileChannel channel = null;
try {
out = new FileOutputStream(file);
channel = out.getChannel();
while (data.hasRemaining()) {
channel.write(data);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
if (channel != null) {
channel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void desserialize(View view) {
FileInputStream fis = null;
FileChannel readChannel = null;
try {
fis = new FileInputStream(file);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
readChannel = fis.getChannel();
int readBytes = 0;
while ((readBytes = readChannel.read(byteBuffer)) != -1) {
System.out.println("读取数据个数:" + readBytes);
}
//把指针回到最初的状态,准备从byteBuffer当中读取数据
byteBuffer.flip();
//解析出二进制为Items对象
textView.append("读取的数据为:
");
Items items = Items.getRootAsItems(byteBuffer);
textView.append("Items:" + items.id() + "," + items.title() + "," + items.show()
+ "," + items.time() + "
");
LetterItems letterItems = items.LetterItems(0);
textView.append("LetterItems:" + letterItems.id() + "," + letterItems.title() + ","
+ letterItems.author() + "," + letterItems.time() + "
");
int length = letterItems.DetailsLength();
for (int i = 0; i < length; i++) {
Details details = letterItems.Details(i);
textView.append("Details:" + details.id() + "," + details.name() + ","
+ details.price() + "," + details.date() + "
");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (readChannel != null) {
readChannel.close();
}
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行效果如下图:
由此可见其序列化速度之快