Android NDK之 跨语言序列化Protocol Buffer 3 编译
关键词:Protobuf NDK CMake JNI
1、protobuf3介绍
Protocol Buffers简称protobuf,由Google开源,是一种跨语言传递对象的一种序列化框架,类似AIDL定义接口一样定义一个通用对象结构描述代码(.proto文件)来生成各语言端的序列化与反序列化的代码,目前在AOSP源码(Android 11)里面也能看到很多.proto
文件,方便在Java和C++之前简化调用逻辑。
2、proto3 语法
protobuf3的使用需要先编写.proto文件来规定需要传递对象所包含的元素,具体语法结构、不同语言之间的数据定义参考: Language Guide (proto 3)
下面定义了一个Person对象来作为例子:
person.proto
proto
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
}
编写好proto后,需要为目标语言生成所需的代码了
3、proto3 编译工具
下载protoc
程序,这里下载的是 protoc-v3.18.1-linux-x86_64 版本 protoc-v3.18.1
解压到工作目录,并创建src文件夹,放入上一步的protoc文件夹
ruby
kryo@WSL1:/mnt/k/Android/NDK-Project/libprotobuf/misc/protoc-3.18.1-linux-x86_64$ tree -L 2
.
├── bin
│ └── protoc
├── include
│ └── google
├── readme.txt
└── src
├── cpp
├── java
└── person.proto
生成java代码
bash
./protoc-3.18.1-linux-x86_64/bin/protoc --cpp_out=src/cpp/ src/person.proto
生成C++代码
bash
./protoc-3.18.1-linux-x86_64/bin/protoc --java_out=src/java/ src/person.proto
src目录查看编译产物
css
src/
├── cpp
│ ├── person.pb.cc
│ └── person.pb.h
├── java
│ └── PersonOuterClass.java
└── person.proto
已经成功生成了cpp和java的源文件,如果打开这些文件可以看到cpp文件还依赖一些protobuf的头文件,java也会依赖protbuf的包,需要进一步集成所需 依赖才能实例化person对象
4、Android集成
Android Studio 的集成可使用脚本自动把.proto文件转化成XXXOuterClass.java文件,这一步先开个坑,暂时利用手动生成
4.1 bulid.gradle增加proto依赖
gradle
implementation 'com.google.protobuf:protobuf-java:3.18.1'
4.2 编写Java端序列化和反序列化代码
把生成的PersonOuterClass.java
放到java源码目录
java
//序列化
byte[] javaByteArray = PersonOuterClass.Person.newBuilder()
.setAge(16)
.setName("Kryo")
.build()
.toByteArray();
//反序列化
try {
PersonOuterClass.Person person = PersonOuterClass.Person.parseFrom(javaByteArray);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
5、NDK 编译 Protobuf3,完成一次C++与Java的对象传递
5.1、源码获取以及CMakeLists.txt编写
5.1.1 下载代码
解压后找到protobuf-3.18.1/src/Makefile.am这个文件 Makefile.am文件找到以下两个变量的源码列表,其中lite版本可以单独编译,两个文件列表一起编译的到完整的libprotobuf
libprotobuf_lite_la_SOURCES
libprotobuf_la_SOURCES
5.1.2 创建cpp目录
demo目录存放protoc生成C++代码
google/protobuf/ 需要把protobuf-3.18.1/src/google目录完整拷贝过来
ruby
kryo@WSL1:/mnt/k/Android/NDK-Project/libprotobuf/src/main/cpp/protobuf3$ tree -L 2
.
├── CMakeLists.txt
├── demo
│ ├── person.pb.cc
│ └── person.pb.h
├── google
│ └── protobuf
└── protobuf_jni.cpp
5.1.3 CMakeList编写
cmake
cmake_minimum_required(VERSION 3.10.2)
project("protobuf3")
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
set(JNI_PATH ${PARENT_DIR})
message("JNI_PATH ${JNI_PATH}")
add_library(protobuf3 SHARED
google/protobuf/any_lite.cc
# ... ...
# libprotobuf_lite_la_SOURCES 的源码列表
# ... ...
google/protobuf/stubs/time.h
google/protobuf/wire_format_lite.cc
################## lite src end ##################
google/protobuf/any.cc
google/protobuf/any.pb.cc
google/protobuf/api.pb.cc
google/protobuf/compiler/importer.cc
# ... ...
# libprotobuf_la_SOURCES 的源码列表
# ... ...
google/protobuf/util/type_resolver_util.cc
google/protobuf/wire_format.cc
google/protobuf/wrappers.pb.cc
demo/person.pb.cc
demo-lite/person-lite.pb.cc
protobuf_jni.cpp
)
add_definitions(-DHAVE_PTHREAD)
target_include_directories(protobuf3 PUBLIC
${JNI_PATH}/protobuf3/
)
target_link_libraries(protobuf3
# List libraries link to the target library
android
log
)
5.2 JNI函数编写
目的:把java序列化的byte数组通过JNI传递到C++ Native层,在Native层反序列化生成对象,处理后再序列化传递到Java层,Java再反序列化生成对 象,完成一次完整的对象传递。
NativeLib.java
java
package com.kryo.libprotobuf;
public class NativeLib {
// Used to load the 'libprotobuf' library on application startup.
static {
System.loadLibrary("protobuf3");
}
public native byte[] process(byte[] data);
}
protobuf_jni.cpp
cpp
//
// Created by X86-TIAN on 2023-11-18.
//
#include <jni.h>
#include <android/log.h>
#include "demo/person.pb.h"
#include "demo-lite/person-lite.pb.h"
#define TAG "Protobuf3-Native"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#ifdef __cplusplus
extern "C"
#endif
JNIEXPORT jbyteArray JNICALL
Java_com_kryo_libprotobuf_NativeLib_process(JNIEnv *env, jobject thiz, jbyteArray inputArray) {
// 获取输入字节数组长度
jsize inputLength = env->GetArrayLength(inputArray);
// 将输入字节数组复制到本地缓冲区
jbyte *inputBuffer = env->GetByteArrayElements(inputArray, nullptr);
Person person;
person.ParseFromArray(inputBuffer, inputLength);
person.set_age(person.age() + 2);
LOGI("Process Java Person name %s", person.name().c_str());
// 创建输出字节数组
jbyteArray outputArray = env->NewByteArray(inputLength);
// 获取输出字节数组指针
jbyte *outputBuffer = env->GetByteArrayElements(outputArray, nullptr);
person.SerializeToArray(outputBuffer, (int) person.ByteSizeLong());
// 释放本地缓冲区
env->ReleaseByteArrayElements(inputArray, inputBuffer, 0);
env->ReleaseByteArrayElements(outputArray, outputBuffer, 0);
// 返回输出字节数组
return outputArray;
}
java端调用
java
byte[] javaByteArray = PersonOuterClass.Person.newBuilder()
.setAge(16)
.setName("Kryo")
.build()
.toByteArray();
Log.d(TAG, "Person Java HEX: 0x" + ByteUtils.bytesToHex(javaByteArray));
NativeLib lib = new NativeLib();
byte[] cppByteArray = lib.process(javaByteArray);
try {
PersonOuterClass.Person person = PersonOuterClass.Person.parseFrom(cppByteArray);
Log.d(TAG, "Process Native Person age " + person.getAge());
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
Log.d(TAG, "Person Native HEX: 0x" + ByteUtils.bytesToHex(cppByteArray));
yaml
2024-04-06 01:59:06.132 25679-25679 JNI_Activity com.kryo.demo D Person Java HEX: 0x0a044b72796f1010
2024-04-06 01:59:06.146 25679-25679 Protobuf3-Native com.kryo.demo I Process Java Person name Kryo
2024-04-06 01:59:06.147 25679-25679 JNI_Activity com.kryo.demo D Process Native Person age 18
2024-04-06 01:59:06.147 25679-25679 JNI_Activity com.kryo.demo D Person Native HEX: 0x0a044b72796f1012
总结
- 本文记录了如何使用ndk编译protobuf库,移植其他开源C++库时也可以参考此文。
- protobuf本质上也是一种TLV编码类型,对于不太复杂的数据交互,自定义一种TLV协议即可,Java用Bytebuffer类封装一下也很方便。
- protobuf的java编译Android Studio可以对其完好支持,C++端等其他端代码也可以使用gradle执行脚本自动化编译,但如果和其他方合作,要求对方适配可能导致对方代码库污染,如果对性能要求没那么苛刻,json够用且方便。