Protobuf在Android上的使用

504 阅读5分钟

简介

  其github主页上有如下介绍:

Protocol Buffers (a.k.a., protobuf) are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data. You can learn more about it in protobuf's documentation.

This README file contains protobuf installation instructions. To install protobuf, you need to install the protocol compiler (used to compile .proto files) and the protobuf runtime for your chosen programming language.

大致翻译为:Protocol Buffers(简称Protobuf) ,是Google出品的序列化框架,与开发语言无关,和平台无关,具有良好的可扩展性。Protobuf和所有的序列化框架一样,都可以用于数据存储、通讯协议。详细信息可查看其官网github。    与传统的序列化框架json、xml相比,其解析更快、数据序列化后更小;同时,由于protobuf序列化是将数据转换为二进制存储,故其适用性更强,可以简单的解析包含二进制结构的数据类型。和其它序列化框架一样,Protobuf在Android中有以下使用场景:

  1. 跨进程通信
    • 从网络端请求数据(通常使用json格式,但Protobuf体积更小,在传输上更具优势)。
    • 通过AIDL等进行进程间通信,protobuf序列化会将数据类转换为byte数组,故可直接用AIDL传递byte数组再进行反序列化。
  2. 数据持久化
    • 将数据结构存储到本地文件(DataStore库就用到了protobuf来对数据进行序列化)。

  后文将详细介绍protobuf使用方式

前言

  在移动终端应用开发中,通常会使用c/c++来实现一些平台通用的数据处理,而JNI(Java Native Interface)则是Java和native的通信的一种跨平台通信方案。

java调用c++方式如下:

     public native void stringToJNI(String str);
extern "C" JNIEXPORT void JNICALL
Java_com_sins_nativelib_NativeLib_stringToJNI(JNIEnv *env, jobject thiz, jstring str) {
    auto cStr = env->GetStringChars(str, nullptr);
    // 业务逻辑
    // ...
    env->ReleaseStringChars(str, cStr);
}

   这样简单的代码即可实现java和本地方法的通信,对于我们来说,我们只需要调用env.GetXXX方法来获取到C++中的基本对象,使用完成调用env.ReleaseXXX即可。但是,若数据结构比较复杂时,应该如何处理呢,即jobject 和 c++ 中结构如何相互转换?以下面的User类为例,在Java和C++中均有其定义如下

class User {
    String mName;
    String mDisplayName;
    String mPassword;
    // ···· other fileds.
} 

  当然,我们可以用如下代码进行 java User -> c User的转换

    jclass userClass = env->GetObjectClass(user);
    jfieldID userNameField = env->GetFieldID(userClass, "mName", "L/java/lang/String;");
    jstring userName = (jstring)env->GetObjectField(user, userNameField);
   // ····

  正如所看的这样,我们需要对每一个属性循环以上操作,他们的语法简单却又复杂,为了传递一个对象,我们需要重复的Coding以上的代码几次甚至几十次,这完全取决于对象的复杂度。相信的大家都不想如此复杂去做,回想在日常开发中,即使跨进程通信-app和服务器之间的通信都没有使用如此复杂的方式,跨进程通信我们将对象序列化后进行传递(即转换为json、xml等格式),在java中有诸如gson、fast-json等库可以一行代码实现 json字符串-object之间的转换。同理,在Jni通信中是我们也可以利用此方式进行处理,但由于c/c++之中没有相对与Java类似的反射机制,我们需要手动构造json字符串,故虽然可以利用json进行通信,但相对而言仍显繁琐。由此引入我们今天的主题 Protonuf`。

在Android JNI开发中使用protobuf

  本文介绍Windows + Android Studio环境下protobuf在Android上的使用。使用前需做以下准备工作:

  • 下载编译好的protoc,由于我是windows 64位机器,故下载protoc-26.1-win64.zip。下载完成后将其解压
  • 编写proto文件,具体语法可查看文档。 此处新建example.proto如下:
syntax = "proto3";
package com.summer.example.protocol;

message UserInfo {
  string userid = 1;
  string deviceid = 2;
}

message Policy {
  bool enable = 1;
  int32 background = 2;
  string describe = 3;
}
  • 依赖配置信息libs.versions.toml如下
[version]
protobuf = "4.28.3"
protobuf-plugin = "0.9.4"

[libraries]
protobuf-kotlin = {group = "com.google.protobuf", name = "protobuf-kotlin", version.ref = "protobuf"}

[plugins]
com-google-protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin"}

  详细步骤如下

  1. 在项目跟build.gradle.kts下增加如下配置:
plugins {
    ...
    alias(libs.plugins.com.google.protobuf) apply false
}
  1. 在模块build.gradle.kts中增加如下配置
import com.google.protobuf.gradle.proto

plugin {
    alias(libs.plugins.com.google.protobuf)
}

android {
    ...
    sourceSets {
            getByName("main") {
                java.srcDirs("src/main/java")
                proto {
                    srcDirs("src/main/proto") //proto文件存储路径,该示例中为example.proto文件存储路径
                }
            }
        }
}

protobuf {
    protoc {
        path = "D:\\Tools\\protoc-26.1-win64\\bin\\protoc.exe" //解压的protoc.exe文件路径

        generateProtoTasks {
            all().forEach { task->
                task.plugins{
                    create("kotlin")
                    create("java")
                }
            }
        }
    }
}
dependencies {
    ...
    implementation(libs.protobuf.kotlin)   
}
  1. build项目。build成功后在/build/generated/source/proto/目录下可以找到对应的java/kotlin源文件
  2. 新增native方法及其调用,通过序列化后的二进制进行参数和返回值传递
class ExampleLib {

    fun getPolicy(userInfo: Example.UserInfo) : Example.Policy {
        val userInfoData = userInfo.toByteArray()
        val policyData = Lib.getPolicy(userInfoData, userInfoData.size)
        return Example.Policy.parseFrom(policyData)
    }

    object Lib {

        init {
            System.loadLibrary("protobuf")
        }

        external fun getPolicy(userInfo: ByteArray, length: Int): ByteArray

    }
}
  1. 下载protobuf源码
  git clone -b [release_tag] https://github.com/protocolbuffers/protobuf.git
  cd protobuf
  git submodule update --init --recursive
  1. 编写cmakeLists.txt文件,主要包括引入libprotobuf及通过Proto.exe和.proto文件生产对应的cpp文件
set(PROTOBUF_PROTOC_EXECUTABLE "D:\\Tools\\protoc-26.1-win64\\bin\\protoc.exe" CACHE STRING "Protoc binary on host")
set(PROTO_GENS_DIR ${CMAKE_CURRENT_BINARY_DIR}/gens)

# 主要为generate_protobuf_sources方法的定义
include(protobuf.cmake)

# 命令行生产.pb.h及.pb.cc文件
generate_protobuf_sources(EXAMPLE_PROTO_SRCS EXAMPLE_PROTO_HDRS ${PROTO_BASE_DIR} ${PROTO_BASE_DIR}/example.proto) 
add_library(example_proto
        SHARED ${EXAMPLE_PROTO_HDRS} ${EXAMPLE_PROTO_SRCS})

target_link_libraries(example_proto PUBLIC libprotobuf)

add_library(${CMAKE_PROJECT_NAME} SHARED
        # List C/C++ source files with relative paths to this CMakeLists.txt.
        ${EXAMPLE_PROTO_HDRS}
        protobuf.cpp
        remote_store.cpp
        remote_store.h)

target_include_directories(${CMAKE_PROJECT_NAME}
        PUBLIC ${PROTO_GENS_DIR} ${PROTOBUF_INCLUDE_DIRS})
target_link_libraries(${CMAKE_PROJECT_NAME}
        PUBLIC example_proto)
  1. 创建jni通信中的C++方法,在该方法中通过proto对应的方法对结构体进行序列化或反序列化。JNI数据的传递通过二进制(即byte数组)的形式
using namespace  com::summer::example::protobuf;;
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_sins_control_protocol_ProtocolLib_getPolicy(
        JNIEnv *env,
        jobject /* this */,
        jbyteArray userinfoData,
        jint len) {
    jboolean isCopy = false;
    auto _userinfo = env->GetByteArrayElements(userinfoData, &isCopy);
    UserInfo info;
    info.ParseFromArray(_userinfo, len);

    // 1 start
    Policy policy;
    RemoteStore::GetPolicy(info, &policy);
    // 1 end
    size_t policyLen = policy.ByteSizeLong();
    char* policyBytes = (char*)malloc(policyLen * sizeof(char));
    policy.SerializeToArray(policyBytes, (int)policyLen);
    jbyteArray array = env->NewByteArray((int)policyLen);
    env->SetByteArrayRegion(array, 0,(int) policyLen, reinterpret_cast<const jbyte *>(policyBytes));
    free(policyBytes);
    return array;
}

  此处未粘贴全部代码,仅描述整体步骤,完整代码及文件可在github查看