写点什么

Android Protobuf 应用及原理,android 输入法开发软键盘切换

用户头像
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.plugins {javalite {}}}}}


apply plugin: 'com.google.protobuf'是 Protobuf 的 Gradle 插件,帮助我们在编译时通过语义分析自动生成源码,提供数据结构的初始化、序列化以及反序列等接口。


compile "com.google.protobuf:protobuf-lite:3.0.0"是 Protobuf 支持库的精简版本,在原有的基础上,用 public 替换 set、get 方法,减少 Protobuf 生成代码的方法数目。

定义数据结构

还是以上面的例子来展开:


syntax = "proto3";package me.ele.demo.protobuf;option java_outer_classname = "LoginInfo";message Login {string account = 1;string password = 2;}


在这里定义了一个LoginInfo,我们只是简单的定义了accountpassword两个字段。这里注意,在上例中, syntax = "proto3";声明 proto 协议版本,proto2 和 proto3 在定义数据结构时有些差别,option java_outer_classname = "LoginInfo";定义了 Protobuf 自动生成类的类名,package me.ele.demo.protobuf;定义了 Protobuf 自动生成类的包名。


通过 Android Studio clean,Protobuf 插件会帮助我们自动生成LoginInfo类,类结构如下:



Protobuf 帮我们自动生成LoginOrBuilder接口,主要声明各个字段的 set 和 get 方法;并且生成Login类,核心逻辑这个类中,通过writeTo(CodedOutputStream)接口序列化到CodedOutputStream,通过 ParseFrom(InputStream)接口从InputStream中反序列化。类图如下:


原理分析

上文提到,Protobuf 不管在时间和空间上更高效,是怎么做到的呢?


消息经过 Protobuf 序列化后会成为一个二进制数据流,通过 Key-Value 组成方式写入到二进制数据流,如图所示:



Key 定义如下:


(field_number << 3) | wire_type


以上面的例子来说,如字段account定义:


string account = 1;


在序列化时,并不会把字段account写进二进制流中,而是把field_number=1通过上述Key的定义计算后写进二进制流中,这就是 Protobuf 可读性差的原因,也是其高效的主要原因。

数据类型


在 Java 种对不同类型的选择,其他的类型区别很明显,主要在与 int32、uint32、sint32、fixed32 中以及对应的 64 位版本的选择,因为在 Java 中这些类型都用 int(long)来表达,但是 protobuf 内部使用 ZigZag 编码方式来处理多余的符号问题,但是在编译生成的代码中并没有验证逻辑,比如 uint 的字段不能传入负数之类的。而从编码效率上,对 fixed32 类型,如果字段值大于 2^28,它的编码效率比 int32 更加有效;而在负数编码上 sint32 的效率比 int32 要高;uint32 则用于字段值永远是正整数的情况。

编码原理

在实现上,Protobuf 使用CodedOutputStream实现序列化、CodedInputStream实现反序列化,他们包含 write/read 基本类型和Message类型的方法,write方法中同时包含fieldNumbervalue参数,在写入时先写入由fieldNumberWireType组成的 tag 值(添加这个WireType类型信息是为了在对无法识别的字段编码时可以通过这个类型信息判断使用那种方式解析这个未知字段,所以这几种类型值即可),这个 tag 值是一个可变长 int 类型,所谓的可变长类型就是一个字节的最高位(msb,most significant bit)用 1 表示后一个字节属于当前字段,而最高位 0 表示当前字段编码结束。在写入 tag 值后,再写入字段值 value,对不同的字段类型采用不同的编码方式:


  1. 对 int32/int64 类型,如果值大于等于 0,直接采用可变长编码,否则,采用 64 位的可变长编码,因而其编码结果永远是 10 个字节,所有说 int32/int64 类型在编码负数效率很低。

  2. 对 uint32/uint64 类型,也采用变长编码,不对负数做验证。

  3. 对 sint32/sint64 类型,首先对该值做 ZigZag 编码,以保留,然后将编码后的值采用变长编码。所谓 ZigZag 编码即将负数转换成正数,而所有正数都乘 2,如 0 编码成 0,-1 编码成 1,1 编码成 2,-2 编码成 3,以此类推,因而它对负数的编码依然保持比较高的效率。

  4. 对 fixed32/sfixed32/fixed64/sfixed64 类型,直接将该值以小端模式的固定长度编码。

  5. 对 double 类型,先将 double 转换成 long 类型,然后以 8 个字节固定长度小端模式写入。

  6. 对 float 类型,先将 float 类型转换成 int 类型,然后以 4 个字节固定长度小端模式写入。

  7. 对 bool 类型,写 0 或 1 的一个字节。

  8. 对 String 类型,使用 UTF-8 编码获取字节数组,然后先用变长编码写入字节数组长度,然后写入所有的字节数组。

  9. 对 bytes 类型(ByteString),先用变长编码写入长度,然后写入整个字节数组。

  10. 对枚举类型(类型值WIRETYPE_VARINT),用 int32 编码方式写入定义枚举项时给定的值(因而在给枚举类型项赋值时不推荐使用负数,因为 int32 编码方式对负数编码效率太低)。

  11. 对内嵌Message类型(类型值WIRETYPE_LENGTH_DELIMITED),先写入整个Message序列化后字节长度,然后写入整个Message


ZigZag 编码实现:(n << 1) ^ (n >> 31) / (n << 1) ^ (n >> 63);CodedOutputStream中还存在一些用于计算某个字段可能占用的字节数的compute静态方法,这里不再详述。


在 Protobuf 的序列化中,所有的类型最终都会转换成一个可变长 int/long 类型、固定长度的 int/long 类型、byte 类型以及 byte 数组。对 byte 类型的写只是简单的对内部 buffer 的赋值:


public void writeRawByte(final byte value) throws IOException {if (position == limit) {refreshBuffer();}buffer[position++] = value;}


对 32 位可变长整形实现为:


public void writeRawVarint32(int value) throws IOException {while (true) {if ((value & ~0x7F) == 0) {writeRawByte(value);return;} else {writeRawByte((value & 0x7F) | 0x80);value >>>= 7;}}}


对于定长,Protobuf 采用小端模式,如对 32 位定长整形的实现:


public void writeRawLittleEndian32(final int value) throws IOException {writeRawByte((value ) & 0xFF);writeRawByte((value >> 8) & 0xFF);writeRawByte((value >> 16)


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


& 0xFF);writeRawByte((value >> 24) & 0xFF);}


对 byte 数组,可以简单理解为依次调用writeRawByte()方法,只是CodedOutputStream在实现时做了部分性能优化。这里不详细介绍。对CodedInputStream则是根据CodedOutputStream的编码方式进行解码,因而也不详述,其中关于 ZigZag 的解码:


(n >>> 1) ^ -(n & 1)

repeated 字段编码

对于repeated字段,一般有两种编码方式:


  1. 每个项都先写入 tag,然后写入具体数据。

  2. 先写入 tag,后 count,再写入 count 个项,每个项包含 length|data 数据。从编码效率的角度来看,个人感觉第二中情况更加有效,然而不知道处于什么原因考虑,Protobuf 采用了第一种方式来编码,个人能想到的一个理由是第一种情况下,每个消息项都是相对独立的,因而在传输过程中接收端每接收到一个消息项就可以进行解析,而不需要等待整个repeated字段的消息包。对于基本类型,Protobuf 也采用了第一种编码方式,后来发现这种编码方式效率太低,因而可以添加[packed = true]的描述将其转换成第三种编码方式(第二种方式的变种,对基本数据类型,比第二种方式更加有效)

  3. 先写入 tag,后写入字段的总字节数,再写入每个项数据。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android Protobuf应用及原理,android输入法开发软键盘切换