Java基础-ProtoBuf解析

Android知识总结

一、Protobuf简介

protocolbuffer(以下简称protobuf)是google 的是一种轻便高效的结构化数据存储格式,作用形同于xml和json。它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。

Protobuf是一种平台无关、语言无关、可扩展且轻便高效的序列化数据结构的协议,可以用于网络通信数据存储。 可简单类比于 XML ,其具有以下特点:

  • 1、语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
  • 2、高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
  • 3、扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程

1、优点

使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。

它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。

2、缺点

Protbuf 与 XML 相比也有不足之处。它功能简单,无法用来表示复杂的概念。

XML 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部使用的工具,在通用性上还差很多。

由于文本并不适合用来描述数据结构,所以 Protobuf 也不适合用来对基于文本的标记文档(如 HTML)建模。另外,由于 XML 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储,除非你有 .proto 定义,否则你没法直接读出 Protobuf 的任何内容。

二、使用protobuf步骤

android中使用protobuf,过程是这样的:

  • 1、定义proto文件;
  • 2、使用该文件生成对应的java类;
  • 3、利用该java类实现数据传输;

从以上过程中就可以看出,我们并不是直接使用proto文件,而是对应的java类,如何根据proto文件生成java类呢?官方推荐的是命令行的方式生成,但是Android Studio生成方式更加简单,这里直接介绍as生成方式(同样适用服务端开发工具intellij idea)

第一步

在AS的Plugins中插入protobuf插件,如图


第二步

在根Project/build.gradle中加入protobuf插件

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
    }
}
第三步

在app/build.gradle中加入如下配置,顶部加上应用插件

apply plugin: 'com.google.protobuf'
apply plugin: 'com.android.application'

android{}中加入

sourceSets {
        main {
            java {
                srcDir 'src/main/java'
            }
            proto {
                srcDir 'src/main/proto' //指定.proto文件路径
                include '**/*.proto'  //find it
            }
        }
    }

android{}同级加入:

//编译并生成
protobuf {
    protoc { // 也可以配置本地编译器路径
        artifact = 'com.google.protobuf:protoc:3.4.0'
    }
    plugins {
        javalite {
            // The codegen for lite comes as a separate artifact
            artifact = 'com.google.protobuf:protobuf-java:3.4.0'
        }
    }
    //这里配置生成目录,编译后会在build的目录下生成对应的java文件和C文件
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                // In most cases you don't need the full Java output
                // if you use the lite output.
                remove java
            }
            task.builtins {
                java {} //java文件
                cpp {} //C文件
            }
        }
    }
}

dependencies中加入protobuf相关依赖

implementation 'com.google.protobuf:protobuf-java:3.4.0'
implementation 'com.google.protobuf:protoc:3.4.0'
  • 混淆配置:
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
第四步

在app\src\main目录中新建proto文件夹,并新建对应的proto文件,这里以LoginRequest.proto为例


LoginRequest.proto文件内容为:

syntax = "proto3";  //声明 proto 协议版本 ( proto2 和 proto3 在定义看数据结构时有些差别)
package com.example.protobuf;  //定义了 Protobuf 自动生成类的包名(即 java 类所在的包名)
option java_package = "com.example.protobuf";//java 类所在的包名 == package com.example.protobuf;
option java_outer_classname = "LoginRequestProto"; //定义了 Protobuf 自动生成类的类名

message LoginRequest {
  string name = 1;
  int32 id = 2;
  string email = 3;
  string phone = 4;
}

//定义了类中的字段(这里只有 account 和 password 两个字段)
message Login {
  uint64 ID = 1;
  string name = 2;
  string password = 3;
  oneof pet {
    Dog dog = 4;
    Cat cat = 5;
  }
}

message Dog {
  string name = 1;
  bool sex = 2;
}

message Cat {
  string name = 1;
  //属性可以与Dog不同
}
第五步

Build/Clean Project跑完即可,此时会在\app\build\generated\source\proto中生成对应的java文件和C++文件,拷出来备用。


三、Window 系统下使用protobuf

protoc --version    #查看protoc的版本

代码转换显例(把目录切换到 E:\user\protoc-3.15.8-win64\bin, protoc的bin目录下)

protoc.exe --java_out=E:\java Immortaldb.proto

输出文件夹是E:\java
输入是Immortaldb.proto

四、简单使用

public class MainActivity extends AppCompatActivity {
    private TextView mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = findViewById(R.id.name_tv);
        //序列化
        LoginRequestProto.LoginRequest loginRequest = LoginRequestProto.LoginRequest
                .newBuilder()
                .setName("XaoLi")
                .setId(122)
                .setEmail("123@QQ.com")
                .setPhone("123456")
                .build();
        byte[] bytes = loginRequest.toByteArray();

        //反序列化
        try {
            LoginRequestProto.LoginRequest login = LoginRequestProto.LoginRequest
                    .parseFrom(bytes);
            mTextView.setText(login.getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 在proto3枚举值第一个必须是0,其他的随意
  • 在proto2,每个属性前必须加required,optional,repeated
  • 该数字只要不重复,可以定义为任何数字,不需要总是从1或者0开始
  • 这个数字表示在序列化数组里面的顺序

注意:定义的proto文件中的编号对应字段名前后可以不行同,但是编号对应的字段类型的相同。

五、语法

syntax = "proto3";
message LoginRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 文件的第一行指定了你正在使用proto3语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。
  • SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。

关键字

  • syntax:声明版本。例如上面syntax="proto3",如果没有声明,则默认是proto2。
  • package:声明包名.
  • import:导入包。类似于java,例如上面导入了timestamp.proto包。
  • java_package:指定生成的类应该放在什么Java包名下。如果你没有显式地指定这个值,则它简单地匹配由package 声明给出的Java包名,但这些名字通常都不是合适的Java包名 (由于它们通常不以一个域名打头)。
  • java_outer_classname:定义应该包含这个文件中所有类的类名。如果你没有显式地给定java_outer_classname ,则将通过把文件名转换为首字母大写来生成。例如上面例子编译生成的文件名和类名是AddressBookProtos。
  • message:类似于java中的class关键字。
  • repeated:用于修饰属性,表示对应的属性是个array。

更多的关键字可以参考官方文档,这里不做介绍。

1)、标量数值类型

Protobuf3语言指南

2)、枚举

当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)并且为每个可能的值定义一个常量就可以了。

在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}

如你所见,Corpus枚举的第一个常量映射为0:每个枚举类型必须将其第一个类型映射为0,这是因为:

  • 必须有有一个0值,我们可以用这个0值作为默认值。
  • 这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。
3)、其他消息类型
message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

所指定的消息字段修饰符必须是如下之一:

  • singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。
  • repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。
    在proto3中,repeated的标量域默认情况虾使用packed。
4)、嵌套类型

你可以在其他消息类型中定义、使用消息类型,在下面的例子中,Result消息就定义在SearchResponse消息内,如:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它,如:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}
5)、字段编号

在message定义中每个字段都有一个唯一的编号,这些编号被用来在二进制消息体中识别你定义的这些字段,一旦你的message类型被用到后就不应该在修改这些编号了。注意在将message编码成二进制消息体时字段编号1-15将会占用1个字节,16-2047将占用两个字节。所以在一些频繁使用用的message中,你应该总是先使用前面1-15字段编号。

你可以指定的最小编号是1,最大是2E29 - 1(536,870,911)。其中19000到19999是给protocol buffers实现保留的字段标号,定义message时不能使用。同样的你也不能重复使用任何当前message定义里已经使用过和预留的字段编号。

参考

Protobuf3语言指南
Language Guide (proto3)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容