前面两篇博客介绍了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
是不同的类型,它们具有不同的特性,因此在互相赋值时需要经过转换。
-
jstring
:jstring
是 JNI 中表示 Java 字符串的类型。它是一个在 Java 和本地代码之间传递字符串数据的中间类型。在 JNI 中,jstring
是一个指向 Java 字符串对象的指针。在 Java 中,字符串是以 UTF-16 编码表示的 -
std::string
:std::string
是 C++ 标准库提供的字符串类型。它是 C++ 中表示字符串的常用类型,用于存储和操作字符数据,std::string
类它是以字节序列的形式存储和操作字符串数据。在使用 C++ 处理字符串时,需要注意字符编码的处理和转换。
2.字段/方法签名
在上面的代码中我们可以看到有如下的形式:
env->GetFieldID(jRequestClass, "name", "Ljava/lang/String;")
这里第二个参数是针对Java对象的成员变量名,第三个参数是该字段的签名字节码。
Java 字段的签名(Field Signature)是用于描述字段类型的字符串表示形式。字段签名包括字段的修饰符、字段类型以及可选的泛型类型信息。
Java 字段的签名遵循一定的规则和符号表示。以下是一些常见的字段签名符号和示例:
-
基本类型:
B
:byteC
:charD
:doubleF
:floatI
:intJ
:longS
:shortZ
: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++ 字符串 |