性能优化之数据传输效率优化

客户端与服务端经常进行着频繁的数据传输,而数据传输又影响着用户体验,本文就传输速率的优化,提出合理的优化建议

传统的传输方案

在开始的时候,采用的是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相对于其他序列化技术,例如XMLJSONProtocol Buffers等,有哪些优势呢?官方文档的说法如下:

  1. 直接读取序列化数据,而不需要解析(Parsing)或者解包(Unpacking):FlatBuffer把数据层级结构保存在一个扁平化的二进制缓存(一维数组)中,同时能够保持直接获取里面的结构化数据,而不需要解析,并且还能保证数据结构变化的前后向兼容
  2. 高效的内存使用和速度:FlatBuffer 使用过程中,不需要额外的内存,几乎接近原始数据在内存中的大小
  3. 灵活:数据能够前后向兼容,并且能够灵活控制你的数据结构
  4. 很少的代码侵入性:使用少量的自动生成的代码即可实现
  5. 强数据类性,易于使用,跨平台,几乎语言无关

    JSON是Android中很常用的数据序列化技术,但却很消耗内存,而FlatBuffer正好解决了这个问题,性能还更好了

使用方法

简单来说:FlatBuffers的使用方法是,首先按照使用特定的IDL定义数据结构schema,然后使用编译工具flatc编译schema生成对应的代码,把生成的代码应用到工程中即可

  • 首先,我们需要得到flatc,这个需要从源码编辑得到。从GitHubClone代码
    1
    git clone https://github.com/google/flatbuffers

首先要使用FlatBuffersIDL定义好数据结构Schema,编写Schema的详细文档在这里。其语法和C语言类似,比较容易上手。我们这里引用一个简单的例子,假设数据结构如下:

1
2
3
4
5
6
class Person {  
String name;
int friendshipStatus;
Person spouse;
List<Person>friends;
}

编写成Schema如下,文件名为Person.fbs

1
2
3
4
5
6
7
8
9
10
11
12
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执行命令如下:

1
./flatc –j -b Person.fbs

在当前目录生成如下文件:

1
2
3
4
5
6
.
└── com
   └── race604
   └── fbs
   ├── FriendshipStatus.java
   └── Person.java

Person类有响应的函数直接获取其内部的属性值,使用非常简单:

1
2
3
4
5
6
7
8
9
10
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",实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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的几点缺点:

  1. FlatBuffers需要生成代码,对代码有侵入性
  2. 数据序列化没有可读性,不方便 Debug
  3. 构建FlatBuffers对象比较麻烦,不直观,特别是如果对象比较复杂情况下需要写大段的代码
  4. 数据的所有内容需要使用Schema严格定义,灵活性不如JSON

所以,在什么情况下选择使用FlatBuffers呢?个人感觉需要满足以下几点:
1.项目中有大量数据传输和解析,使用JSON成为了性能瓶颈
2.稳定的数据结构定义

用一个完整例子说明

假如存在一个数据结构Items,Items里面有很多属性,其中Items又包含LetterItems,LetterItems又有自己的属性还包含Details,其又有自己的属性,那么这样一个结构应该怎样去写成fbs文件呢?编写文本文件,其后缀名要为.fbs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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还支持enumunionstruct的定义
windows平台可以直接下载flatc.exe使用:https://github.com/google/flatbuffers/releases
使用如下命令生成文件

1
flatc --java test.fbs

执行上述命令会生成三个java文件:Items.javaLetterItems.javaDetails.java
将生成的文件和FlatBufferBuilder.java以及Table.java复制到项目目录中
适当修改包名和一些引用错误,就完美融入到项目中了
接下来做一个简单测试,序列化然后写入本地,之后再读取出来显示出来
这里做了一个简单布局。一个TextView用来显示,两个Button,一个解析,一个读取,为了方便,直接使用了onClick属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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\n");
textView.append("写入的数据为:\n");
textView.append("Item(1,article,false,20180101L,*)\n");
textView.append("LetterItems(1,title,author,20180101L,*)\n");
textView.append("Details(1,organge,5.0,20180101L)\n");
textView.append("Details(2,apple,5.0,20180101L)\n");

//保存文件到本地
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("读取的数据为:\n");

Items items = Items.getRootAsItems(byteBuffer);
textView.append("Items:" + items.id() + "," + items.title() + "," + items.show()
+ "," + items.time() + "\n");

LetterItems letterItems = items.LetterItems(0);
textView.append("LetterItems:" + letterItems.id() + "," + letterItems.title() + ","
+ letterItems.author() + "," + letterItems.time() + "\n");

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() + "\n");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (readChannel != null) {
readChannel.close();
}
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

运行效果如下图:

由此可见其序列化速度之快

Donate comment here