前の 2 つのブログでは、jni 関連のコンテンツと Android での使用方法を紹介しましたが、デモは比較的簡単です。今回は、Java と C/C++ の間のより複雑な相互呼び出しについて説明しましょう。
次に実装する関数は、Java オブジェクトを C++ に渡し、次に C++ オブジェクトを使用して値を受け取り、最後に C++ オブジェクトの値を Java 層に戻すことです。
1. コード例
1. Javaエンティティクラスを作成する
public class RequestBean {
public String name;
public int num;
}
public class ResponseBean {
public String resName;
public int resNum;
}
2. ネイティブメソッドを定義する
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. プロジェクトを再ビルドし、.so を生成して、Java レイヤーで呼び出します。
RequestBean bean = new RequestBean();
bean.name = "张三";
bean.num = 0;
ResponseBean resp = JNIDemo.getRespFromCPP(bean);
tvMsg.setText("name: " + resp.resName + " num:"+resp.resNum);
効果:
完全なプロジェクト構造:
2. 要点の分析
1. jstring とstd::
string
JNIjstring
とC++ではstd::string
型が異なり、特性も異なるため、値を代入する際には変換する必要があります。
-
jstring
:jstring
JNIにおけるJava文字列を表す型です。これは、Java とネイティブ コード間で文字列データを渡すための中間型です。JNI では、jstring
Java String オブジェクトへのポインター。Java では、文字列は UTF-16 エンコーディングで表されます。 -
std::string
:std::string
C++の標準ライブラリが提供する文字列型です。これは、C++ で文字列を表現するための一般的な型で、文字データの保存と操作に使用され、std::string
文字列データをバイト シーケンスの形式で保存および操作します。C++ を使用して文字列を処理する場合は、文字エンコーディングの処理と変換に注意する必要があります。
2. フィールド/メソッドのシグネチャ
上記のコードでは、次のフォームが表示されます。
env->GetFieldID(jRequestClass, "name", "Ljava/lang/String;")
ここで、2 番目のパラメータは Java オブジェクトのメンバー変数名で、3 番目のパラメータはフィールドの署名バイトコードです。
Java フィールドの署名 (フィールド署名) は、フィールドの型を記述するために使用される文字列表現です。フィールド署名には、フィールドの修飾子、フィールド タイプ、およびオプションのジェネリック タイプ情報が含まれます。
Java フィールドの署名は、特定のルールと表記法に従います。一般的なフィールド署名記号と例をいくつか示します。
-
基本タイプ:
B
:バイトC
:文字D
:ダブルF
:浮くI
:intJ
:長さS
:短いZ
:ブール値
-
参照タイプ:
L
+ クラス名+;
: 参照型を示します。クラス名は/
区切り文字としてスラッシュ ( ) を使用し、;
セミコロン ( ) で終わる必要があります。たとえば、 タイプLjava/lang/String;
を表すため です。java.lang.String
-
配列タイプ:
[
: 1次元配列を表します[[
: 2次元配列を表します。[
同様に、多次元配列は複数の値を加算することで表現できます。- 配列型の後には、対応する要素型の署名が続きます。たとえば、タイプ
[Ljava/lang/String;
を示しString[]
、タイプ[[I
を示しますint[][]
。
-
汎用タイプ:
- 囲まれた型パラメータ リストを使用し
<>
、,
複数の型パラメータを区切るにはコンマ ( ) を使用します。たとえば、 タイプLjava/util/List<Ljava/lang/String;>;
を表すため です。List<String>
- 囲まれた型パラメータ リストを使用し
フィールド署名は、Java リフレクション、バイトコード操作、クラス ローダーなどのシナリオで、さまざまな種類のフィールドを記述および区別するためによく使用されます。
メソッドのシグネチャ
Java メソッド シグネチャ (メソッド シグネチャ) は、メソッドを記述するために使用される文字列表現です。メソッド シグネチャには、メソッド名、パラメータ リスト、戻り値の型が含まれます。
メソッド シグネチャのバイトコード表現は次のとおりです。
(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;
フィールド シグネチャとメソッド シグネチャ (メソッド シグネチャ) にはいくつかの違いがあることに注意してください。フィールド シグネチャは主にフィールドの型の記述に焦点を当てますが、メソッド シグネチャには戻り値の型、パラメータ リスト、例外が含まれます。情報。
3.ReleaseStringUTFChars _
JNI では、ReleaseStringUTFChars 関数を使用して、GetStringUTFChars 関数で取得した jstring オブジェクトの UTF-8 エンコード文字配列を解放します。これら 2 つの関数は、適切なメモリ管理を確保し、メモリ リークを回避するために、多くの場合一緒に使用されます。
GetStringUTFChars 関数は、jstring オブジェクトの UTF-8 エンコード文字の配列へのポインターを返します。文字配列は使用中は変更されない必要があり、関連するメモリは使用されなくなったら解放する必要があります。
ReleaseStringUTFChars 関数の機能は、JNI が文字配列を使用する必要がなくなったことを JVM (Java 仮想マシン) に通知し、それに関連付けられたメモリ リソースを解放することです。これにより、メモリ リークを回避し、占有されていたメモリ領域を解放できます。
4. 関連する jni ライブラリ関数の分析
上記のコードでは、Java オブジェクトに関する情報を取得するために多数の jni ライブラリ関数が使用されており、これらの関数はすべて jni.h ファイルで宣言されています。一般的に使用される関数の説明をいくつか示します。
jclass FindClass(const char* name) |
指定されたクラスパス内の対応する Java クラスを検索して返します。 |
jclass GetObjectClass(ジョブオブジェクトobj) |
オブジェクトを通じてこのクラスを取得します |
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig) |
指定したクラスのフィールドのIDを取得します。 |
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig) |
指定したクラスのメソッドIDを取得します。 |
ジョブジェクト GetObjectField(ジョブジェクト obj, jfieldID フィールド ID) |
フィールドIDに基づいて指定されたオブジェクトオブジェクトを取得します |
jobject NewObject(jclass clazz, jmethodID メソッドID, ...) |
新しい Java オブジェクトを作成する |
jstring NewStringUTF(const char* バイト) |
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 のシグネチャとまったく同じであり、非互換性の問題はありません。