• Android项目中使用Protobuf数据格式


    Protobuf介绍

    Protobuf全称:Protocol Buffers,是Google推出的一种与平台无关,语言无关,可扩展的轻便高效的序列化数据存储格式,类似于我们常用的xml和json。
    问题:既然有了xml和json,Google为啥还要推出Protobuf呢?
    存在是有原因的,Protobuf 用两个字总结:小,快。用 Protobuf 序列化后的大小是 json 的十分之一,是 xml 格式的二十分之一,而且性能是他们的 5~100 倍。
    通常情况下,我们使用 xml 或者 json 进行数据通信是没什么问题的,但是在高性能和大数据通信的情况下,如果有办法压缩数据量,提高传输效率,显然会给用户带来更快更流畅的体验。

    xmljsonprotobuf
    数据保存方式文本文本二进制
    数据保存大小一般
    解析效率一般

    Protobuf环境配置

    方式一:GitHub下载Protobuf并安装
    方式二:brew安装

    使用AndroidStudio创建Protobuf文件

    1.创建一个以.proto为后缀的文件,如下图:
    在这里插入图片描述
    可以看到这个文件前面的 icon 是个文本类型的,而且你在编写代码时,没有任何提示,关键字也没高亮显示,此时我们需要集成一个插件来支持 Protobuf 的编写
    2.集成Protocol Buffers插件

    当我们集成这个插件后,可以看到文件icon和代码高亮的变化,而且还会有语法提示

    Protobuf语法基础

    Protobuf常用关键字介绍

    关键字说明
    syntax指定Protobuf的版本,Protobuf目前有proto2和proto3两个常用版本,如果没有声明,则默认是proto2
    package指定文件包名
    import导包,和Java的import类似
    message定义消息类,和Java的class关键字类似,消息类之间可以嵌套
    repeated定义一个集合,和Java的集合类似
    reserved保留字段,如果使用了这个关键字修饰,用户就不能使用这个字段编号或字段名
    optionoption可以用在Protobuf的scope中,或者message,enum,service的定义中,Protobuf定义的option由java_package,java_outer_classname,java_multiple_files 等等
    optional表示该字段是可选的
    java_package指定生成类所在的包名,需配合
    java_outer_classname定义当前文件的类名,如果没有定义,则默认为文件的首字母大写名称
    java_multiple_files指定编译过后 Java 的文件个数,如果是 true,那么将会一个 Java 对象一个类,如果是 false,那么定义的Java 对象将会被包含在同一个文件中

    基本数据类型
    在这里插入图片描述
    可变长度编码和固定长度编码区别:

    //例如说我在 Java 里面进行如下定义:
    int a = 1;
    //因为 int 类型占 4 个字节,1个字节占 8 位,我们把 1 的字节占位给列出来:
    00000000 00000000 00000000 00000001
    //可以看到 1 的前面 3 个字节占位都是 0,在 Protobuf 里面是可以去掉的,于是就变成了:
    00000001
    //因此 1 在 Protobuf 里面就只占用了一个字节,节省了空间

    上面这种就是可变长度编码。而固定长度编码就是即使前面的字节占位是0,也不能去掉,我就是要占这么多字节

    基本数据类型默认值

    在这里插入图片描述

    消息类型定义

    在上面创建的Protobuf文件中,定义一个学生,学生有姓名,年龄,邮箱和课程

    //指定 Protobuf 版本
    syntax = "proto3";
    
    //指定包名
    package erdai;
    
    //定义一个学生的消息类
    message Student{
      //姓名
      string name = 1;
      //年龄
      int32 age = 2;
      //邮箱
      string email = 3;
      //课程
      repeated string course = 4; //相当于 Java 的 List
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    1、一个Protobuf文件里面可以添加多个消息类,也可以进行嵌套
    2、上面的1,2,3,4并不是给字段赋值,而是给每个字段定义一个唯一的编号。这些编号用于二进制格式中标识你的字段,并且在使用你的消息类型后不应更改。
    3、1-15的字段编号只占一个字节进行编码,16-2047的字段编号占两个字节,包括字段编号和字段类型,因此建议更多的使用1-15的字段编号
    4、可以指定最小字段编号为 1,最大字段编号为 2^29 - 1 或 536870911。另外不能使用 19000-19999 的标识号,因为 protobuf 协议实现对这些进行了预留,同样,也不能使用任何以前保留(reserved) 的字段编号

    Protobuf枚举类型

    message Weather{
      int32 query = 1;
    
      //季节
      enum Season{
        //允许对枚举常量设置别名
        option allow_alias = true;
        //枚举里面的 = 操作是对常量进行赋值操作
        //春
        SPRING = 0;
        //夏
        SUMMER = 1;
        //秋 如果不设置别名,不允许存在两个相同的值
        FALL = 2;
        AUTUMN = 2;
        //冬
        WINTER = 3;
      }
    
      //对 season 进行编号
      Season season = 2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1、定义枚举类型使用enum关键字
    2、枚举类型第一个字段的值必须为0,否则编译会报错
    3、枚举常量值必须在32位整型值的范围内。因为 enum 值是使用可变编码方式的,对负数不够高效,因此不推荐在 enum 中使用负数

    4、枚举里面的 = 操作是对常量进行赋值操作,而枚举外面的 = 则是对当前字段进行编号

    Protobuf集合

    repeated string list = 1; //类似 Java 的 List
    
    map<string,string> = 2; //类似 Java 的 Map
    
    • 1
    • 2
    • 3

    1、Protobuf 中定义集合,就是在你定义好的属性前面加 repeated 关键字

    2、Protobuf 中定义 map 和 Java 类似,只不过 map 是小写的

    reserved保留字段

    当我们使用reserved关键字指定一个字段编号或字段名为保留字段后,我们就不能去使用它,否则编译器会报错,如下:
    在这里插入图片描述

    Protobuf转Java文件

    首先我们要明白一点:Protobuf是一种与平台,语言无关的数据存储格式,因此我们要在其他语言如:Java,Kotlin,Dart等语言中使用它,则必须将Protobuf文件转换为对应平台的语言文件去使用,这里我们转Java文件为例,介绍两种转换的方式:
    1、集成Protobuf插件转换
    2、使用protoc命令行转换
    在使用这两种方式转换前,我们需要先集成 protobuf-java 这个第三方库,因为我们转换的 Java 文件中会使用这个库里面的功能

    "com.google.protobuf:protobuf-java:3.7.1"
    
    • 1

    集成Protobuf插件转换

    1、在项目的根build.gradle文件中,添加如下依赖

    buildscript {
        repositories {
            gradlePluginPortal()
        }
        dependencies {
            classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.19'
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2、在app的build.gradle文件中应用插件

    //1、如果使用的是 apply 方式,Android 插件必须在 Protobuf 插件之前,如下:
    apply plugin: 'com.android.application'
    apply plugin: 'com.google.protobuf'
    
    //2、如果使用的是 Gradle DSL 方式,则与顺序无关
    plugins {
        id 'com.google.protobuf'
        id 'com.android.application'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3、在app的build.gradle文件中配置protobuf编译任务

    android {
        //...
    }
    
    protobuf {
        //配置 protoc 编译器
        protoc {
            artifact = 'com.google.protobuf:protoc:3.19.2'
        }
        //配置生成目录,编译后会在 build 的目录下生成对应的java文件
        generateProtoTasks {
            all().each { task ->
                task.builtins {
                    remove java
                }
                task.builtins {
                    java {}
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4、指定protobuf文件所在路径

    android {
        //...
    
        sourceSets {
            main {
                //实际测试指不指定无所谓,不影响 Java 文件生成
                proto {
                    srcDir 'src/main/proto'
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    经过上面的4步,我们的插件就算配置完成了,接下来我们测试一下
    5、重新编译项目
    以我们上面那个student.proto为例,里面的内容如下:

    //指定 Protobuf 版本
    syntax = "proto3";
    
    //指定包名
    package erdai;
    
    //定义一个学生的消息类
    message Student{
      //姓名
      string name = 1;
      //年龄
      int32 age = 2;
      //邮箱
      string email = 3;
      //课程
      repeated string course = 4;
    
    }
    
    //定义一个天气的消息类
    message Weather{
      int32 query = 1;
    
      //季节
      enum Season{
        option allow_alias = true;
        //春
        SPRING = 0;
        //夏
        SUMMER = 1;
        //秋
        FALL = 2;
        AUTUMN = 2;
        //冬
        WINTER = 3;
      }
    
      Season season = 2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    编译后,会在 app/build/generated/source/proto/ 目录下生成如下文件:
    在这里插入图片描述
    这种方式会自动给我们生成对应的Java类

    使用protoc命令行转换

    执行如下命令:

    protoc --java_out=${"你要生成的 Java 文件目录"} ${"Protobuf 文件位置"}
    
    //以我的为例:
    protoc --java_out=./app/src/main/java ./app/src/main/proto/student.proto
    
    • 1
    • 2
    • 3
    • 4

    生成的文件:
    在这里插入图片描述
    这种方式的优点就是使用简单,但是每一次创建新的Protobuf文件都需要执行一下命令

    生成的Java文件介绍

    1、生成的类StudentOuterClass有4个子类:

    Student(实现了StudentOrBuilder接口)
    Weather(实现WeatherOrBuilder接口)
    StudentOrBuilder(接口)
    WeatherOrBuilder(接口)

    2、Student是StudentOuterClass的静态内部类,构造方法私有化,需通过Builder模式创建对象。另外Student提供了属性的get方法,无set方法,Builder作为Student的静态内部类提供了属性的get,set,clear方法,clear就是清空某个属性回到初始值。
    3、Weather 同2

    Protobuf使用

    接下来我们对刚才生成的Java类进行使用

    基本调用

    fun main(){
        //构建 Protobuf 对象
        val student = StudentOuterClass.Student.newBuilder()
            .setName("erdai")
            .setAge(18)
            .setEmail("erdai666@qq.com")
            .addAllCourse(mutableListOf("Math", "English", "Computer"))
            .build()
        println(student)
        println()
        println(StudentOuterClass.Weather.Season.SPRING.number)
        println(StudentOuterClass.Weather.Season.SUMMER.number)
        println(StudentOuterClass.Weather.Season.AUTUMN.number)
        println(StudentOuterClass.Weather.Season.WINTER.number)
    }
    
    //打印结果
    name: "erdai"
    age: 18
    email: "erdai666@qq.com"
    course: "Math"
    course: "English"
    course: "Computer"
    
    
    0
    1
    2
    3
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    序列化和反序列化

    fun main(){
        //1、构建 Protobuf 对象
        val student = StudentOuterClass.Student.newBuilder()
            .setName("erdai")
            .setAge(18)
            .setEmail("erdai666@qq.com")
            .addAllCourse(mutableListOf("Math", "English", "Computer"))
            .build()
        //2、序列化并返回一个包含其原始字节的字节数组
        val byteArray: ByteArray = student.toByteArray()
        //3、反序列化从字节数组中解析消息
        val parseStudent: StudentOuterClass.Student = StudentOuterClass.Student.parseFrom(byteArray)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Protobuf和Json相互转换

    首先我们需要添加一个第三方依赖:

    implementation 'com.google.protobuf:protobuf-java-util:3.19.2'
    
    • 1

    Protobuf转Json

    fun main(){
        //1、构建 Protobuf 对象
        val student = StudentOuterClass.Student.newBuilder()
            .setName("erdai")
            .setAge(18)
            .setEmail("erdai666@qq.com")
            .addAllCourse(mutableListOf("Math", "English", "Computer"))
            .build()
        //2、Protobuf 转 Json
        val json = JsonFormat.printer().print(student)
        //3、打印 json
        println(json)
    }
    
    //打印结果
    {
      "name": "erdai",
      "age": 18,
      "email": "erdai666@qq.com",
      "course": ["Math", "English", "Computer"]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    Json转Protobuf

    fun main(){
        //1、构建 Protobuf 对象
        val student = StudentOuterClass.Student.newBuilder()
            .setName("erdai")
            .setAge(18)
            .setEmail("erdai666@qq.com")
            .addAllCourse(mutableListOf("Math", "English", "Computer"))
            .build()
        //2、Protobuf 对象转 Json
        val json = JsonFormat.printer().print(student)
        //3、Json 转 Protobuf 对象
        val builder = StudentOuterClass.Student.newBuilder()
        JsonFormat.parser().ignoringUnknownFields().merge(json,builder)
        //4、打印 Protobuf 对象
        println(builder.build())
    }
    
    //打印结果
    name: "erdai"
    age: 18
    email: "erdai666@qq.com"
    course: "Math"
    course: "English"
    course: "Computer"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    Protobuf和Java对象相互转换

    当我们将Protobuf转换为JSON后,在把JSON转换为对应的Java对象,这里我们先引入Gson:

    implementation 'com.google.code.gson:gson:2.8.9'
    
    • 1

    定义一个接收当前JSON的数据模型:

    data class MyStudent(
        var name: String? = "",
        var age: Int = 0,
        var email: String? = "",
        var course: MutableList<String>? = null
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最后进行转换:

    fun main(){
        //1、构建 Protobuf 对象
        val student = StudentOuterClass.Student.newBuilder()
            .setName("erdai")
            .setAge(18)
            .setEmail("erdai666@qq.com")
            .addAllCourse(mutableListOf("Math", "English", "Computer"))
            .build()
        //2、将 Protobuf 转换为 Json
        val json = JsonFormat.printer().print(student)
    
        //3、将 Json 转换为 Java Bean 对象
        val myStudent = Gson().fromJson(json,MyStudent::class.java)
        println(myStudent)
    }
    
    //打印结果
    MyStudent(name=erdai, age=18, email=erdai666@qq.com, course=[Math, English, Computer])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
  • 相关阅读:
    OceanBase!一场专为开发者打造的数据库技术交流盛宴!
    D. Divide and Sum(组合数学)
    Windows安全日志分析
    【python】用Pillow操作图片
    【C# 调试】.net中的 .pdb文件是什么,有什么用
    【无标题】
    GoConvey中Convey嵌套时的执行顺序
    安全漏洞修复集
    卡片布局以及鼠标悬浮展示全部
    python 定时器
  • 原文地址:https://blog.csdn.net/ChenYiRan123456/article/details/132662620