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够用且方便。
相关推荐
雨白12 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk12 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING12 小时前
RN容器启动优化实践
android·react native
恋猫de小郭15 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker20 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴20 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭1 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe2 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农2 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos