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够用且方便。