Android NDK之 跨语言序列化Protocol Buffer 3 编译

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够用且方便。
相关推荐
服装学院的IT男4 小时前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
Arms2064 小时前
android 全面屏最底部栏沉浸式
android
服装学院的IT男4 小时前
【Android 源码分析】Activity生命周期之onStop-1
android
ChinaDragonDreamer6 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
网络研究院9 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
凉亭下9 小时前
android navigation 用法详细使用
android
小比卡丘11 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭12 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss14 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.14 小时前
数据库语句优化
android·数据库·adb