https://developers.google.cn/protocol-buffers/docs/javatutorial
https://developers.google.cn/protocol-buffers/docs/proto
https://github.com/protocolbuffers/protobuf
https://zh.wikipedia.org/wiki/Protocol_Buffers
https://github.com/google/protobuf-gradle-plugin
1什么是Protobuf?
google开源项目。序列化数据结构的方案,通常用于编写需要数据交换或者需要存储数据的程序。这套方案包含一种用于描述数据结构的接口描述语言(Interface Description Language)和一个生成器,用于生成描述该数据结构的不同编程语言的源代码。
序列化与反序列化
序列化 : 将数据结构或对象转换成二进制串的过程
反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程
特点:
优点:
性能:
体积小:序列化后,数据大小可缩小3-10倍
序列化速度快:比XML和JSON快20-100倍
传输速度快:因为体积小,传输起来带宽和速度会有优化
使用
应用场景
传输数据量大 & 网络环境不稳定 的数据存储、RPC 数据交换 的需求场景
如 即时IM (QQ、微信)的需求场景
在 传输数据量较大的需求场景下,Protobuf比XML、Json 更小、更快、使用 & 维护更简单!
使用Gradle插件
https://github.com/google/protobuf-gradle-plugin
1 修改项目根目录下build.gradle
dependencies下增加classpath ‘com.google.protobuf:protobuf-gradle-plugin:0.8.8’
2 修改app目录下build.gradle
apply plugin: 'com.android.application’后加上apply plugin: ‘com.google.protobuf’
3 然后加入protobuf配置(与android{}同级)
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.0.0'
}
plugins {
javalite {
artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
remove java
}
task.plugins {
javalite { }
}
}
}
}
4 最后dependencies下加入implementation ‘com.google.protobuf:protobuf-lite:3.0.0’
dependencies {
...
implementation 'com.google.protobuf:protobuf-lite:3.0.0'
}
官方例子:
https://developers.google.cn/protocol-buffers/docs/javatutorial
在main下新建proto文件夹,新建test.proto,复制以下代码。
syntax = "proto2";
package tutorial;
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;//数组
}
message AddressBook {
repeated Person people = 1;
}
点击make project
然后在app/build/generated/source/proto/debug/javalite/com/example/tutorial目录下可以看到生成了一个AddressBookProtos文件
接下来看下与Gson和fastjson的对比:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
protoTest();
}
private void protoTest() {
AddressBookProtos.Person.PhoneNumber.Builder builder
= AddressBookProtos.Person.PhoneNumber.newBuilder().setNumber("110");
AddressBookProtos.Person.Builder zs = AddressBookProtos.Person.newBuilder()
.setName("张三")
.setId(1)
.addPhones(builder);
AddressBookProtos.Person.PhoneNumber.Builder builder1
= AddressBookProtos.Person.PhoneNumber.newBuilder().setNumber("120");
AddressBookProtos.Person.Builder ls = AddressBookProtos.Person.newBuilder()
.setName("李四")
.setId(2)
.addPhones(builder1);
AddressBookProtos.AddressBook addressBook = AddressBookProtos.AddressBook.newBuilder()
.addPeople(zs)
.addPeople(ls).build();
long l = System.currentTimeMillis();
byte[] bytes = addressBook.toByteArray();
Log.e(TAG, "protobuf 序列化耗时:" + (System.currentTimeMillis() - l));
Log.e(TAG, "protobuf 序列化数据大小:" + bytes.length);
try {
l = System.currentTimeMillis();
AddressBookProtos.AddressBook.parseFrom(bytes);
Log.e(TAG, "protobuf 反序列化耗时:" + (System.currentTimeMillis() - l));
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
JsonTest.fastJson();
JsonTest.gson();
}
}
导入依赖
implementation 'com.google.code.gson:gson:2.8.2'
implementation 'com.alibaba:fastjson:1.1.67.android'
import android.location.Address;
import android.util.Log;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.google.gson.Gson;
import java.io.Serializable;
public class JsonTest {
private static final String TAG = "JsonTest";
public static void fastJson() {
AddressBook addressBook = getObject();
long l = System.currentTimeMillis();
String data = JSON.toJSONString(addressBook);
byte[] bytes = data.getBytes();
Log.e(TAG, "FastJson 序列化耗时:" + (System.currentTimeMillis() - l));
Log.e(TAG, "FastJson 序列化数据大小:" + bytes.length);
l = System.currentTimeMillis();
AddressBook addressBook1 = JSON.parseObject(new String(bytes), AddressBook.class);
Log.e(TAG, "FastJson 反序列化耗时:" + (System.currentTimeMillis() - l));
// Log.e(TAG,addressBook1.getPersons().get(0).getName());
// Log.e(TAG,addressBook1.getPersons().get(0).getPhones().get(0).getNumber());
}
public static void gson(){
AddressBook addressBook = getObject();
long l = System.currentTimeMillis();
Gson gson = new Gson();
String data = gson.toJson(addressBook);
byte[] bytes = data.getBytes();
Log.e(TAG, "Gson 序列化耗时:" + (System.currentTimeMillis() - l));
Log.e(TAG, "Gson 序列化数据大小:" + bytes.length);
l = System.currentTimeMillis();
AddressBook addressBook1 = gson.fromJson(new String(bytes), AddressBook.class);
Log.e(TAG, "Gson 反序列化耗时:" + (System.currentTimeMillis() - l));
// Log.e(TAG,addressBook1.getPersons().get(0).getName());
// Log.e(TAG,addressBook1.getPersons().get(0).getPhones().get(0).getNumber());
}
private static AddressBook getObject(){
AddressBook addressBook = new AddressBook();
PhoneNumber p_110 = new PhoneNumber();
p_110.setNumber("110");
Person zs = new Person();
zs.setId(1);
zs.setName("张三");
zs.addPhones(p_110);
addressBook.addPersons(zs);
PhoneNumber p_120 = new PhoneNumber();
p_120.setNumber("120");
Person ls = new Person();
ls.setId(2);
ls.setName("李四");
ls.addPhones(p_120);
addressBook.addPersons(ls);
return addressBook;
}
}
import java.util.ArrayList;
import java.util.List;
public class AddressBook {
List<Person> persons;
public AddressBook() {
this.persons = new ArrayList<>();
}
public void addPersons(Person person) {
persons.add(person);
}
public List<Person> getPersons( ) {
return persons;
}
}
import java.util.ArrayList;
import java.util.List;
public class Person {
private String name;
private int id;
private String email;
private List<PhoneNumber> phones;
public Person() {
phones = new ArrayList<>();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public void addPhones(PhoneNumber number) {
phones.add(number);
}
public List<PhoneNumber> getPhones() {
return phones;
}
}
package com.hongx.protobuf;
public class PhoneNumber {
enum PhoneType {
MOBILE,
HOME,
WORK;
}
private String number;
private PhoneType type;
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public PhoneType getType() {
return type;
}
public void setType(PhoneType type) {
this.type = type;
}
}
MainActivity: protobuf 序列化耗时:51
MainActivity: protobuf 序列化数据大小:38
MainActivity: protobuf 反序列化耗时:15
JsonTest: FastJson 序列化耗时:116
JsonTest: FastJson 序列化数据大小:119
JsonTest: FastJson 反序列化耗时:62
JsonTest: Gson 序列化耗时:163
JsonTest: Gson 序列化数据大小:119
JsonTest: Gson 反序列化耗时:6
2 Protobuf序列化原理
2.1 protobuf数据结构
TLV存储方式
TAG-Length-Value
标识-长度-字段值
protobuf数据编码存储原理
对数据进行编码 + 存储
速度快:编解码方式简单(简单数学运算)
数据体积小:
对于不同数据类型 采用不同的 序列化方式(编码方式 & 数据存储方式)
将 消息里的每个字段 进行编码后,再利用T - L - V 存储方式 进行数据的存储,最终得到的是一个 二进制字节流
Varint编码方式
图:Varint编码方式
图:Varint解码过程
对于int32 / int64 类型的字段值(正数),Protocol Buffer直接采用 Varint编码
对于sint32 / sint64 类型的字段值(负数),Protocol Buffer会先采用 Zigzag 编码,再采用 Varint编码
Zigzag编码方式
T-V存储方式&编码原理
新建:HxTest.proto
syntax = "proto2";
package test;
option java_package = "com.hx.test";
option java_outer_classname = "HxTestProtos";
message HxTest{
//wire_type=0, field_number=1
//Tag = (field_number << 3) | wire_type
// TAG=8
required int32 id1 = 1;
//wire_type=0, field_number=2
//Tag = (field_number << 3) | wire_type
// TAG=16
required int32 id2 = 2;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
HxTestProtos.HxTest test = HxTestProtos.HxTest.newBuilder()
.setId1(300)
.setId2(296)
.build();
byte[] bytes = test.toByteArray();
Log.e(TAG, Arrays.toString(bytes));
// protoTest();
}
Log输出:
MainActivity: [8, -84, 2, 16, -88, 2]
protobuf整个编码流程如下图:
核心源码分析
protobuf-3.8.0\java\core\src\main\java\com\google\protobuf\CodedOutputStream.java[1724行]
@Override
public void writeUInt32NoTag(int value) throws IOException {
try {
while (true) {
if ((value & ~0x7F) == 0) {
buffer.put((byte) value);
return;
} else {
buffer.put((byte) ((value & 0x7F) | 0x80));
value >>>= 7;
}
}
} catch (BufferOverflowException e) {
throw new OutOfSpaceException(e);
}
}
0x7F: 0111 1111
~0x7F: 1000 0000
value
& 1000 0000
------------------
1000 0000
或 0000 0000:value<128 能被1个字节存进去
value
& 0111 1111
-----------------------
0??? ????
| 1000 0000
-----------------------
1??? ????
3 网络通信协议中使用
通讯协议分层模型
粘包和分包问题
自定义协议
4总结
1 尽量使用多用 optional或 repeated修饰符
因为若optional 或 repeated 字段没有被设置字段值,那么该字段在序列化时的数据中是完全不存在的,即不需要进行编码
2字段标识号(Field_Number)尽量只使用 1-15,且不要跳动使用
因为Tag里的Field_Number是需要占字节空间的。如果Field_Number>16时,Field_Number的编码就会占用2个字节,那么Tag在编码时也就会占用更多的字节;如果将字段标识号定义为连续递增的数值,将获得更好的编码和解码性能
3若需要使用的字段值出现负数,请使用 sint32 / sint64,不要使用int32 / int64
因为采用sint32 / sint64数据类型表示负数时,会先采用Zigzag编码再采用Varint编码,从而更加有效压缩数据
4 对于repeated字段,尽量增加packed=true修饰
因为加了packed=true修饰repeated字段采用连续数据存储方式,即T - L - V - V -V方式