安卓JNI从0到1入门教程(三)

前面两篇博客介绍了jni相关内容,以及怎么在Android中简单使用,demo比较简单。这次来讲讲复杂一点的java和C/C++的互相调用。

下面我们将要实现的功能是将Java对象传递给C++,然后用C++的对象接收值,最后把C++对象的值回传给Java层。

一、代码示例

1.创建java实体类

public class RequestBean {
    public String name;
    public int num;
}

public class ResponseBean {
    public String resName;
    public int resNum;
}

2.定义native方法

public class JNIDemo {

    static {
        //这个库名必须跟Android.mk的LOCAL_MODULE对应,如果是第三方so,也请对应正确
        System.loadLibrary("JniDemo");
    }

    public static native String test();

    public static native ResponseBean getRespFromCPP(RequestBean request);
}

3.生成头文件com_example_jni_JNIDemo.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jni_JNIDemo */

#ifndef _Included_com_example_jni_JNIDemo
#define _Included_com_example_jni_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_jni_JNIDemo
 * Method:    test
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_jni_JNIDemo_test
  (JNIEnv *, jclass);

/*
 * Class:     com_example_jni_JNIDemo
 * Method:    getRespFromCPP
 * Signature: (Lcom/example/jni/RequestBean;)Lcom/example/jni/ResponseBean;
 */
JNIEXPORT jobject JNICALL Java_com_example_jni_JNIDemo_getRespFromCPP
  (JNIEnv *, jclass, jobject);

#ifdef __cplusplus
}
#endif
#endif

4.声明C++类头文件CResponse.h

#include "string"//string在C++中并不是一个基本类型,而是一个完整的字符串类。要使用需要include其头文件
using std::string; //声明使用空间

class CResponse{
private:
    string name;
    int num;
public:

    string getName();

    int getNum();

    void setValue(string name,int num);
};

5.C++类实现源文件CResponse.cpp

#include "CResponse.h"
#include "string"

using namespace std;

string CResponse::getName() {
    return this->name;
}

int CResponse::getNum() {
    return this->num;
}

void CResponse::setValue(string name, int num) {
    this->name = name;
    this->num = num;
}

6.JNI实现JNITest.cpp

#include "com_example_jni_JNIDemo.h" //引入刚刚生成的头文件
#include "CResponse.h"
#include "string"

extern "C"
JNIEXPORT jstring JNICALL Java_com_example_jni_JNIDemo_test(JNIEnv * env, jclass clz){
    return env->NewStringUTF("hello world");
}

//jstring转C++ std::string
std::string jstringToString(JNIEnv* env, jstring jstr)
{
    const char *cStr = env->GetStringUTFChars(jstr, nullptr);
    std::string cppStr(cStr);  //这是string.h提供的库函数
    env->ReleaseStringUTFChars(jstr, cStr);//释放掉cStr,防止内存泄漏
    return cppStr;
}

extern "C"
JNIEXPORT jobject JNICALL Java_com_example_jni_JNIDemo_getRespFromCPP(JNIEnv * env, jclass clz, jobject request) {

    //获取传过来的java对象值
    // 1)获取java对象的jclass;
    jclass  jRequestClass = env->FindClass("com/example/jni/RequestBean");
    // 2)获取java对象的字段ID,注意字段名称和签名;
    jfieldID nameId  = env->GetFieldID(jRequestClass, "name", "Ljava/lang/String;");
    // 3)根据字段ID获取该字段的值;
    jstring name = (jstring)env->GetObjectField(request, nameId);
    jfieldID numId  = env->GetFieldID(jRequestClass, "num", "I");
    jint cNum = env->GetIntField(request, numId);

    CResponse *cResp = new CResponse();
    // Java jstring类型转C++ string类型
    string cName = jstringToString(env,name) + " from c++"; //从java获取到name后拼上字符串
    cNum++; //将java对象传过来的num值加1
    //调用函数赋值给C++对象的成员变量
    cResp->setValue(cName,cNum);

    //C++对象转换为java对象
    // 1)获取java ResponseBean对象的jclass;
    jclass jRespClass = env->FindClass("com/example/jni/ResponseBean");
    // 2)获取构造方法ID;
    jmethodID jmId = env->GetMethodID(jRespClass, "<init>", "()V");
    // 3)通过构造方法ID创建Java ResponseBean对象;
    jobject jReturnObj = env->NewObject(jRespClass, jmId);
    // 4)获取ReturnInfo对象的字段ID;
    jfieldID rNameId = env -> GetFieldID(jRespClass, "resName", "Ljava/lang/String;");
    jfieldID rNumId = env -> GetFieldID(jRespClass, "resNum", "I");

    // 5)通过字段ID给每个字段赋值
    jstring rName = env->NewStringUTF(cResp->getName().c_str());
    env->SetObjectField(jReturnObj, rNameId, rName);
    env->SetIntField(jReturnObj, rNumId, cResp->getNum());
    // 返回Java对象;
    return jReturnObj;
}

7.在CMakeLists.txt加入库配置

#指定CMake的最低版本要求
cmake_minimum_required(VERSION 3.18.1)

# 定义本地库的名称
set(my_lib_name JniDemo)

#添加库配置,如果有多个库,可以添加多个add_library方法
add_library( # 指定生成库的名称,使用上面定义的变量
        ${my_lib_name}
        # 标识是静态库还是动态库
        SHARED
        # C/C++源代码文件路径
        src/main/cpp/JNITest.cpp
        src/main/cpp/CResponse.cpp)

#指定.h头文件的目录
include_directories(src/main/cpp/)

# 指定构建输出路径
set_target_properties(${my_lib_name} PROPERTIES
        LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}")

8.Rebuild一下项目,生成.so,然后在Java层调用

RequestBean bean = new RequestBean();
bean.name = "张三";
bean.num = 0;
ResponseBean resp = JNIDemo.getRespFromCPP(bean);
tvMsg.setText("name: " + resp.resName + " num:"+resp.resNum);

效果:

完整项目结构: 

二、关键点解析

1.jstring和std::string

在 JNI 中,jstring 和 C++ 的 std::string 是不同的类型,它们具有不同的特性,因此在互相赋值时需要经过转换。

  1. jstring jstring 是 JNI 中表示 Java 字符串的类型。它是一个在 Java 和本地代码之间传递字符串数据的中间类型。在 JNI 中,jstring 是一个指向 Java 字符串对象的指针。在 Java 中,字符串是以 UTF-16 编码表示的

  2. std::string std::string 是 C++ 标准库提供的字符串类型。它是 C++ 中表示字符串的常用类型,用于存储和操作字符数据,std::string 类它是以字节序列的形式存储和操作字符串数据。在使用 C++ 处理字符串时,需要注意字符编码的处理和转换。

 2.字段/方法签名

在上面的代码中我们可以看到有如下的形式:

env->GetFieldID(jRequestClass, "name", "Ljava/lang/String;")

这里第二个参数是针对Java对象的成员变量名,第三个参数是该字段的签名字节码。

Java 字段的签名(Field Signature)是用于描述字段类型的字符串表示形式。字段签名包括字段的修饰符、字段类型以及可选的泛型类型信息。

Java 字段的签名遵循一定的规则和符号表示。以下是一些常见的字段签名符号和示例:

  • 基本类型:

    • B:byte
    • C:char
    • D:double
    • F:float
    • I:int
    • J:long
    • S:short
    • Z:boolean
  • 引用类型:

    • L + 类名 + ;:表示引用类型,类名需要使用斜杠(/)作为分隔符,并以分号(;)结尾。例如,Ljava/lang/String; 表示 java.lang.String 类型。
  • 数组类型:

    • [:表示一维数组
    • [[:表示二维数组
    • 以此类推,可以通过添加多个 [ 表示多维数组
    • 数组类型后面跟着对应元素类型的签名。例如,[Ljava/lang/String; 表示 String[] 类型,[[I 表示 int[][] 类型。
  • 泛型类型:

    • 使用 <> 括起来的类型参数列表,多个类型参数之间使用逗号(,)分隔。例如,Ljava/util/List<Ljava/lang/String;>; 表示 List<String> 类型。

字段签名在 Java 反射、字节码操作、类加载器等场景中经常使用,用于描述和区分不同类型的字段。

方法签名

Java 方法签名(Method Signature)是用于描述方法的字符串表示形式。方法签名包括方法名称、参数列表和返回类型。

方法签名的字节码表示形式如下:

(L参数类型1;L参数类型2;...;)返回类型

如果方法有声明抛出异常,则方法签名中的异常信息使用 ^ 符号表示,后跟异常类的表示形式。

例如:

  • public void printMessage(String message) 的字节码表示形式为:(Ljava/lang/String;)V
  • private int calculateSum(int[] numbers) 的字节码表示形式为:([I)I
  • protected boolean checkValidInput(String username, String password) 的字节码表示形式为:(Ljava/lang/String;Ljava/lang/String;)Z
  • public void process() throws IOException 的字节码表示形式为:()V^Ljava/io/IOException;

需要注意的是,字段签名和方法签名(Method Signature)有一些差异,字段签名主要关注字段类型的描述,而方法签名则包括返回值类型、参数列表和异常信息等。

3.ReleaseStringUTFChars

在 JNI 中,ReleaseStringUTFChars 函数用于释放由 GetStringUTFChars 函数获取的 jstring 对象的 UTF-8 编码的字符数组。这两个函数通常一起使用,以确保正确管理内存和避免内存泄漏。

GetStringUTFChars 函数返回一个指向 jstring 对象 UTF-8 编码字符数组的指针。该字符数组在使用过程中需要保持不变,并且需要在不再使用时释放相关内存。

ReleaseStringUTFChars 函数的作用是通知 JVM(Java 虚拟机),JNI 不再需要使用该字符数组,并释放与之关联的内存资源。这样可以避免内存泄漏,释放占用的内存空间。

4.相关jni库函数解析

上述代码中使用了大量jni库函数来获取Java对象相关信息,这些函数都被声明在jni.h文件。下面列举一些常用的函数说明:

jclass FindClass(const char* name)
在指定的类路径中查找并返回对应的 Java 类
jclass GetObjectClass(jobject obj)
通过对象获取这个类
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
获取指定类的字段的 ID
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
获取指定类的方法的 ID
jobject GetObjectField(jobject obj, jfieldID fieldID)
根据字段ID获取指定object对象
jobject NewObject(jclass clazz, jmethodID methodID, ...)
创建一个新的 Java 对象
jstring NewStringUTF(const char* bytes)
将 C/C++ 字符串转换为 Java 字符串
const char* GetStringUTFChars(jstring string, jboolean* isCopy)
将 Java 字符串转换为 C/C++ 字符串
void ReleaseStringUTFChars(jstring string, const char* utf)
释放通过 GetStringUTFChars() 获得的 C/C++ 字符串

后记

在kotlin中使用JNI

kotlin与java可以互相调用,那么对JNI也一样,在Java中用native关键字声明方法,在kotlin中则用external关键字,下面是一个示例:

class KNIDemo {

    companion object {
        init {
            System.loadLibrary("JniDemo")
        }
    }

    external fun test(): String
    external fun getRespFromCPP(request: RequestBean?): ResponseBean?
}

其他方面跟Java使用一样。Kotlin的成员变量和方法的签名方式与Java完全一致,也不存在不兼容问题。

猜你喜欢

转载自blog.csdn.net/gs12software/article/details/131681348