Android JNI 0 から 1 入門チュートリアル (3)

前の 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型が異なり、特性も異なるため、値を代入する際には変換する必要があります。

  1. jstring: jstring JNIにおけるJava文字列を表す型です。これは、Java とネイティブ コード間で文字列データを渡すための中間型です。JNI では、jstringJava String オブジェクトへのポインター。Java では、文字列は UTF-16 エンコーディングで表されます。

  2. 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:int
    • J:長さ
    • 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 のシグネチャとまったく同じであり、非互換性の問題はありません。

おすすめ

転載: blog.csdn.net/gs12software/article/details/131681348