在iOS上使用Protocol Buffer
[TOC]
一、定义
Protocol Buffer是Google出品的一种轻量级、高效的结构化数据存储格式(类似于XML、Json)
二、作用
通过将 结构化的数据 进行 序列化,从而时间 数据存储/RPC数据交换的功能
序列化:将数据结构或对象转换成二进制串的过程
-
反序列化:将在序列化过程正所生成的二进制串转换成数据结构或者对象的过程
三、特点
相对于常见的XML、Json数据存储格式,Protocol Buffer有如下特点:
1、优点:
- 体积小:序列化后,数据大小可缩小约3倍
- 序列化速度快:比XML和JSON快20~100倍
- 传输速度快:因为体积小,传输起来带宽和速度会有优化
- 使用简单:proto编译器自动进行序列化和反序列化
- 维护成本低:多平台仅需维护一套对象协议文件(.proto)
- 向后兼容性好:机扩展性好,不必破坏旧数据格式就可以直接对数据结构进行更新
- 加密性好:http传输内容抓包只能看到字节
2、缺点:
- 功能方面:不适合用于基于文本的标记文档建模,因为文本不适合描述数据结构
- 通用性较差:JSON、XML已经成为多钟行业标准的编写工具,而Protobuf只是Google公司内部使用工具
- 自解释性差:以二进制数据流方式存储,需要通过.proto文件才能了解到数据结构
3、总结:
Protocol Buffer比XML、JSON更小、更快、使用和维护更简单
四、应用场景
一般在传输数据量较大的需求场景下(比如即时IM)
五、环境配置及使用流程
1、前期准备:
官网地址:https://developers.google.com/protocol-buffers/ ,
PB目前托管在GitHub的链接地址:https://github.com/google/protobuf 。
从上面GitHub链接下载压缩包解压,得到 protobuf-master 文件夹。源码的主要功能可以分为两部分:
- PB编译器:源码生成器,将PB格式定义文件 .proto,转换为对象源码(支持C++、JAVA、OC等)。
- PB基础库:完成对象 —> 二进制数据的序列化、二进制数据—>对象的反序列化过程的支持。
2、PB编译器的安装:
使用终端 cd 到 ../protobuf-master/objectivec/DevTools/* 中
运行 full_mac_build.sh* 脚本,从而安装PB编译器
-
成功之后,会生成 *../protobuf-master/src/protoc* 文件,这个就是我们要的PB编译器了
$ cd /Users/lailingwei/Desktop/protobuf-master/objectivec/DevTools $ ./full_mac_build.sh
3、PB基础库的导入:
- 把除了 objectivec/GPBProtocolBuffers.m* 之外的所有 objectivec.h & objectivec.m 文件,以及 objectivec/google 文件夹,全部复制到我们新建的一个文件夹内,比如命名为 Protobuf
- 把 Protobuf 拖入到我们的工程中去
- 在工程:targets —> Build Phases —> Compile Sources 中,把刚拖进去的所有 .m 文件,设置为 -fno-objc-arc 从而关闭这些文件的ARC支持
- 在工程设置搜索静态块头文件:targets —> Build Setting —> Search Paths —> Header Search Paths,写入 $(PROJECT)/Protobuf
4、通过Protocol Buffer 编译器 编译 .proto 文件
- 编写文件 xxx.proto
- 在终端 cd 到 xxx.proto 文件所在目录,使用命令让编译器吧 xxx.proto 编译生成 xxx.h 和 xxx.m 文件
- 把生成的 xxx.h 和 xxx.m 文件拖入工程,同时要对拖进去的 .m 文件关闭ARC支持
$ cd /Users/lailingwei/Desktop/protobuf-master/examples
$ protoc --plugin=/Desktop/protobuf-master/src/protoc-gen-objc addressbook.proto --objc_out="./"
六、构建Protocol Buffer消息对象模型:
新建一个文件,命名为:Demo.proto,并根据数据结构的需求,在Demo.proto里,通过Protocol Buffer语法写入对应.proto对象模型的代码,如下:
syntax = "proto3" // 表示使用protobuf的编译器版本
// 关注1:包名
package protocobuff_Demo;
// 关注2:option选项
option java_package = "com.example.tutorial";
option java_outer_classname = "Demo";
// 关注3:消息模型
message Persion {
string name = 1;
int32 id = 2; // Unique ID number for this person
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2[default = HOME];
}
PhoneNumber phone = 4;
}
message AddressBook {
Person person = 1;
}
下面结合上述栗子,对Protocol Buffer语法进行介绍:
关注1:包名
// 关注1:包名
package protocobuff_Demo;
主要用于防止不同.proto项目间命名发生冲突
关注2:Option选项
// 关注2:option选项
option java_package = "com.example.tutorial";
option java_outer_classname = "Demo";
主要影响特定环境下的处理方式,但不改变整个文件声明的含义
常用Option选项如下:
/*
定义:Java包名
作用:指定生成的类应该放在什么Java包名下
注:如不显式指定,默认包名为:按照应用名称倒序方式进行排序
*/
option java_package = "com.example.tutorial";
/*
定义:类名
作用:生成对应.java文件的类名(不能跟下面message的类名相同)
注:如不显式指定,则默认为把.proto文件名转换为首字母大写来生成
如.proto文件名="my_proto.proto",默认情况下,将使用“MyProto”做为类名
*/
option java_outer_classname = "Demo";
/*
作用:影响 C++ & java 代码的生成
xxx 参数如下:
1、SPEED(默认):protocol buffer 编译器将通过在消息类型上执行序列化、语法分析及其他统一的操作
2、CODE_SIZE:编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其他操作
特点:采用该方式产生的代码将比SPEED要少很多,但是效率较低;
使用场景:常用在 包含大量.proto文件 但 不追求效率 的应用中。
3、LITE_RUNTIME:编译器依赖于运行时 核心类库 来生成代码(即采用libprotobuf-lite代替libprotobuf)
特点:这种核心类库要比全类库小得多(忽略了一些描述符及反射);编译器采用该模式产生的方法实现与SPEED模式不相上下,产生的类通过实现MessageLite接口,但它仅仅是MEssager接口的一个子集
使用场景:移动手机平台应用
*/
option optimize_for = xxx;
/*
作用:定义在C++、java、python中,Protocol buffer编译器是否应该 基于服务定义 产生 抽象服务代码
*/
option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
/*
如果该选项在一个整型基本类型上被设置为真,则采用更紧凑的编码方式(不会对数值造成损失)
*/
optional repeated int32 samples = 4[packed=true];
/*
作用:判断该字段是否已经被弃用
作用同 在java中注释@Deprecated
*/
optional int32 old_field = 6[deprecated=true];
关注3:消息模型
作用:真正用于描述数据结构
3.1、消息对象
在 ProtocolBuffers 中:
一个消息对象(Message) = 一个 结构化数据
消息对象用 修饰符 message 修饰
-
消息对象 含有 字段:消息对象(Message)里的 字段 = 结构化数据 里的成员变量
特别注意:
- 在一个 .proto 文件中可定义多个消息对象
- 一个消息对象里可以定义另外一个消息对象(即嵌套)
a.添加:在一个 .proto 文件中可定义多个消息对象
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
// 与SearchRequest消息类型 对应的 响应消息类型SearchResponse
message SearchResponse {
...
}
b.一个消息对象 里 可以定义 另外一个消息对象(即嵌套)
message Person {
string name = 1;
int32 id = 2;
string email = 3;
/*
该消息类型定义在 Person消息类型的内部
即Person消息类型 是 PhoneNumber消息类型的父消息类型
*/
message PhoneNumber {
string number = 1;
}
}
/*多重嵌套*/
message Outer { // level 0
message MiddleAA { // level 1
message Inner { // level 2
int64 ival = 1;
bool booly = 2;
}
}
}
3.2、字段
消息对象的自动组成主要是:字段 = 字段修饰符 + 字段类型 + 字段名 + 标识号 (required string name = 1;)
protobuf3之后,字段前取消了required和optional两个关键字,目前可使用的只有repeated。
a、字段修饰符
required:表示改字段必须赋值(>=1个)
optional:表示改字段可选赋值,即 可设置/不设置(<=1个),如果可选字段解析时没有值,则使用默认值(自定义默认值 / 系统默认值)
自定义默认值在消息描述文件中指定,如optional int32 rewsult_per_page = 3[default=10];
系统默认值规则:数字类型 = 0,字符串类型 = 空字符串,bools值 = false, 枚举类型:枚举类型定义中的第一个值
对于内嵌消息,默认值 = 消息的“默认实例(default instance)”或“原型(prototype)”,且无字段集;
若调用accessor获取未设置的optional字段的值,返回的是字段的默认值。
-
repeated:表示该字段可被重复赋值(无限制);
- 重复的值的顺序会被保留,相当于动态变化的数组
- 注意:由于历史原因,repeated的字段 并没有被高效编码,所以用户应该使用特殊选项;[packed=true]来保证更高效的编码,如:repeated int32 samples = 4[packed=true]。
b、字段类型
字段类型主要有三类:
- 基本数据类型
- 枚举类型
- 消息对象类型
message Person {
// 基本数据类型 字段
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
PhoneType type = 2[default = HOME];
// 枚举类型 字段
}
// 消息类型 字段
PhoneNumber phone = 4;
}
message ComplexObject {
google.protobuf.Any any = 7; // Any对象
map map = 8; // 定义Map对象
}
// 定义Map的value值
message MapValue {
string mapValue = 1;
}
b.1、基本数据类型
.proto 基本数据类型 对应于 各平台的基本数据类型如下:
b.2、枚举类型
作用:为字段指定一个可能取值的字段集合
-
说明:如下面例子,电话号码 可能是手机号、家庭电话号或工作电话号的其中一个,那么就将PhoneType定义为枚举类型,并将加入电话的集合(MOBILE、HOME、WORK)
// 枚举类型需要先定义才能进行使用 // 枚举类型 定义 enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } /* 1、枚举类型的定义可在一个消息对象的内部或外部 2、都可以在同一 .proto 文件中的任务消息对象里使用 3、当枚举类型是在一个消息内部定义,希望再 另一个消息中 使用是,需要采用MessageType.EnumType的语法格式 */ message PhoneNumber { string number = 1; PhoneType type = 2 [default = HOME]; } /* 特别注意: 1、枚举常量必须在32位整型值的范围内 2、不推荐在enum中使用负数:因为enum值是使用可变编码方式的,对负数不够高 */
当对一个 使用了枚举类型的 .proto 文件 使用 Protocol Buffer 编译器编译时,生成代码文件中:
- 对 Java 或 C++ 来说,将有一个对应的 enum 文件
- 对 Python 来说,有一个特殊的 EnumDescriptor 类
b.3、消息对象 类型
一个消息对象 可以将 其他消息对象类型 用作字段类型,情况如下:
- 使用同一个 .proto 文件里的消息类型
- 使用 内部消息类型, 先在 消息类型 中定义 其他消息类型,然后再使用
message Person {
string name = 1;
int32 id = 2;
string email = 3;
/*
该消息类型 定义在 Person消息类型的内部
即Person消息类型 是 PhoneNumber 消息类型的父消息类型
*/
message PhoneNumber {
string number = 1;
}
// 直接使用内部消息类型
PhoneNumber phone = 4;
}
- 使用外部消息类型 即外部重用,需要 用作字段类型的消息类型 定义在 改消息类型外部
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
message AddressBook {
// 直接使用了 Person 消息类型作为消息字段
Person person = 1;
}
- 使用 外部消息的内部消息类型
message Person {
string name = 1;
int32 id = 2;
string email = 3;
// PhoneNumber消息类型 是 Person消息类型的内部消息类型
message PhoneNumber {
string number = 1;
PhoneType type = 2 [default = HOME];
}
}
/*
若父消息类型外部的消息类型需要重用改内部消息类型
需要以 Parent.Type 的形式去使用
*/
// PhoneNumber父消息类型Person 的外部 OtherMessage消息类型需要使用 PhoneNumber消息类型
message OtherMessage {
Person.PhoneNumber phonenumber = 1;
}
- 使用不同的 .proto 文件里的消息类型,在 A.proto 文件通过导入(import) B.proto 文件中来使用 B.proto 文件里的消息类型
/*
在 A.proto 文件中添加 B.proto 文件路径的导入声明
Protocol Buffer编译器会在该目录中查找需要被导入的 .proto 文件
如果不提供参数,编译器就在 其调用的目录下查找
*/
import "myproject/other_protos.proto"
c. 字段名
该字段的名称,此处不做过多描述。
d.标识号
-
作用:通过二进制格式唯一标识每个字段
- 一旦开始使用就不能够再改变
- 标识号使用范围:[1,2的29次方 - 1]
- 不可使用 [19000 - 19999] 标识号,因为 Protobuf 协议实现中对这些标识号进行了预留。
-
编码占有内存规则:每个字段在进行编码时都会占用内存,而占用内存大小取决于标识号:
- 范围 [1,15] 标识号的字段 在编码时占用 1 个字节;
- 范围 [15,2047] 标识号的字段 在编码时占用 2 个字节;
-
使用建议
为频繁出现的消息字段保留 [1,15] 的标识号
-
为将来有可能添加的、频繁出现的消息字段预留 [1,15] 标识号
e. 关于字段的高级用法:
目的:为了满足新需求,需要更新 消息类型 而不破坏已有消息类型代码
-
更新字段时,需要符合下列规则:
f.保留标识符
如果你通过删除或者注释所有域,以后的用户可以重用标识号当你重新更新类型的时候。如果你使用旧版本加载相同的.proto文件,这会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法,就是指定保留标识符,pb的编译器会警告未来尝试使用这些域标识符的用户。
message Foo {
reserved 2,15,9 to 11;
reserved "foo","bar";
}