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

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

传统的传输方案

在开始的时候,采用的是xml传输,这就要使用到Serializable/Parcelable序列化以及反序列化,其传输速度之慢,基本已经被遗弃,后来又出现了JSON序列化传输,其常用工具就是GSON和fastjson,但随着时代的进步,json也体现出了局限性

json的局限性主要体现在其是基于字符串的传输,在转换的时候会生成大量JsonObject,然后转化为字符串,送进流里面,然后传输,在服务端也要从流中取出,然后反序列化,一大堆繁琐的过程,其也渐渐不适合当今传输数据的要求

那么什么样的方案才满足当今数据传输的要求呢?

新的数据传输方式

现在有如下选择可以用

  • Protocal Buffers:强大,灵活,但是对内存的消耗会比较大,并不是移动终端上的最佳选择
  • Nano-Proto-Buffers:基于Protocal,为移动终端做了特殊的优化,代码执行效率更高,内存使用效率更佳
  • FlatBuffers:这个开源库最开始是由Google研发的,专注于提供更优秀的性能

以下两幅图是这三个工具的性能对比
性能优化-数据传输效率优化_第1张图片
性能优化-数据传输效率优化_第2张图片
可见,FlatBuffers几乎从空间和时间复杂度上完胜其他技术
FlatBuffers是一个开源的跨平台数据序列化库,可以应用到几乎任何语言(C++,C#,Go,Java,JavaScript,PHP,Python),最开始是Google为游戏或者其他对性能要求很高的应用开发的。项目地址在GitHub上。官方的文档在这里

FlatBuffer的优点
FlatBuffer相对于其他序列化技术,例如XMLJSONProtocol Buffers等,有哪些优势呢?官方文档的说法如下:

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

使用方法

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

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

首先要使用FlatBuffersIDL定义好数据结构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文章的一张图如下:
性能优化-数据传输效率优化_第4张图片
可见,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

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
使用如下命令生成文件

flatc --java test.fbs

执行上述命令会生成三个java文件:Items.javaLetterItems.javaDetails.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\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();
            }
        }
    }
}

运行效果如下图:
性能优化-数据传输效率优化_第5张图片
由此可见其序列化速度之快

你可能感兴趣的:(Android)