- 学习了如何在C++中调用Java的一些类和方法,例如System.currentTimeMillis(), Integer.parseInt(), String.substring()和ArrayList。
- 学习了如何使用JNI函数来查找类、获取方法ID、调用方法、创建对象、传递参数和返回值。
- 学习了如何使用JNI函数来检查、清除、打印和抛出Java异常。
- 学习了如何使用命名空间、常量和辅助函数来组织和简化的C++代码。
- 学习了如何在Kotlin中定义和调用原生方法,并使用System.loadLibrary()来加载本地库。
4.1 JNI接口方法表
- JNI接口方法表是一个结构体指针,它包含了JNI提供的所有函数的指针。JNI接口方法表的类型是JNIEnv,它是一个二级指针,指向一个JNIEnv_结构体,该结构体只有一个成员,即指向JNI函数表的指针。
- JNI接口方法表是线程相关的,每个线程都有自己的JNIEnv。在原生函数中,第一个参数就是JNIEnv,可以通过它调用JNI函数。例如:
JNIEXPORT void JNICALL Java_com_example_MyClass_nativeMethod(JNIEnv *env, jobject obj) { // 调用JNI函数 jclass cls = (*env)->FindClass(env, "java/lang/String"); // ... }
- 在C++中,JNIEnv被定义为一个类,它包含了一个指向JNI函数表的指针,并且为每个JNI函数提供了一个成员函数,可以直接调用。例如:
JNIEXPORT void JNICALL Java_com_example_MyClass_nativeMethod(JNIEnv *env, jobject obj) { // 调用JNI函数 jclass cls = env->FindClass("java/lang/String"); // ... }
- JNIEnv的类型是一个指向JNI接口方法表的指针,它的定义如下:
typedef const struct JNINativeInterface *JNIEnv;
- JNI接口方法表的定义如下:
JNINativeInterface接口方法表(随便找的
struct JNINativeInterface { void *reserved0; void *reserved1; void *reserved2; void *reserved3; jint (JNICALL *GetVersion)(JNIEnv *env); jclass (JNICALL *DefineClass) (JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len); jclass (JNICALL *FindClass) (JNIEnv *env, const char *name); /* ... more functions ... */ };
- JNI接口方法表中的函数可以分为以下几类:
- 版本信息:GetVersion
- 类操作:DefineClass, FindClass, GetSuperclass, IsAssignableFrom等
- 异常处理:Throw, ThrowNew, ExceptionOccurred, ExceptionDescribe等
- 全局引用操作:NewGlobalRef, DeleteGlobalRef等
- 局部引用操作:NewLocalRef, DeleteLocalRef等
- 弱全局引用操作:NewWeakGlobalRef, DeleteWeakGlobalRef等
- 监视器操作:MonitorEnter, MonitorExit等
- Java对象操作:AllocObject, NewObject等
- 字段访问:GetFieldID, GetObjectField等
- 方法调用:GetMethodID, CallObjectMethod等
- 数组操作:NewArray, GetArrayLength等
- 字符串操作:NewStringUTF, GetStringUTFChars等
- 直接缓冲区操作:NewDirectByteBuffer, GetDirectBufferAddress等
4.2 JNI基础API的使用
- JNI基础API是一些常用的JNI函数,它们可以实现以下功能:
- 查找Java类、字段和方法的ID
- 调用Java实例方法和静态方法
- 获取和设置Java实例字段和静态字段的值
- 创建Java对象和数组
- 操作Java字符串和原始类型数组
- 抛出和处理Java异常
- JNI基础API的函数名都遵循一定的命名规则,例如:
- FindClass: 查找一个Java类。
- GetMethodID / GetStaticMethodID: 获取Java方法的ID。
- CallMethod / CallStaticMethod: 调用Java方法。
- GetFieldID / GetStaticFieldID: 获取Java字段的ID。
- GetField / SetField: 获取或设置Java字段的值。
- 其中,表示返回值或参数的类型,可以是以下之一:
- Boolean: 布尔型
- Byte: 字节型
- Char: 字符型
- Short: 短整型
- Int: 整型
- Long: 长整型
- Float: 浮点型
- Double: 双精度浮点型
- Object: 对象型
- Void: 空类型
- 下面是一些JNI基础API的使用示例:
// 查找java.lang.String类 jclass stringClass = (*env)->FindClass(env, "java/lang/String"); // 获取java.lang.String类的构造方法ID,参数为字节数组和编码名称 jmethodID stringConstructor = (*env)->GetMethodID(env, stringClass, "<init>", "([BLjava/lang/String;)V"); // 获取java.lang.String类的length方法ID,无参数,返回值为整型 jmethodID stringLength = (*env)->GetMethodID(env, stringClass, "length", "()I"); // 创建一个字节数组对象,长度为10 jbyteArray byteArray = (*env)->NewByteArray(env, 10); // 填充字节数组对象的内容为"Hello JNI" jbyte buf[10] = {'H', 'e', 'l', 'l', 'o', ' ', 'J', 'N', 'I', '\0'}; (*env)->SetByteArrayRegion(env, byteArray, 0, 10, buf); // 创建一个字符串对象,表示"UTF-8"编码 jstring encoding = (*env)->NewStringUTF(env, "UTF-8"); // 调用java.lang.String类的构造方法,创建一个字符串对象,内容为"Hello JNI" jstring str = (*env)->NewObject(env, stringClass, stringConstructor, byteArray, encoding); // 调用java.lang.String类的length方法,获取字符串的长度 jint len = (*env)->CallIntMethod(env, str, stringLength); // 打印字符串的长度 printf("The length of the string is %d\n", len);
4.3 JNI异常API的使用
- JNI异常API是一些用于处理Java异常的JNI函数,它们可以实现以下功能:
- 检查是否发生了Java异常
- 清除Java异常
- 获取Java异常对象
- 抛出Java异常
- 抛出新的Java异常
- JNI异常API的函数名都以Exception开头,例如:
- ExceptionOccurred: 检查是否发生了Java异常。
- ExceptionClear: 清除Java异常。
- ExceptionDescribe: 打印Java异常的堆栈跟踪信息。
- ExceptionGetCause: 获取Java异常的原因对象。
- Throw: 抛出Java异常对象。
- ThrowNew: 抛出新的Java异常。
- 下面是一些JNI异常API的使用示例:
// 查找java.lang.Integer类 jclass integerClass = (*env)->FindClass(env, "java/lang/Integer"); // 获取java.lang.Integer类的parseInt方法ID,参数为字符串,返回值为整型 jmethodID parseInt = (*env)->GetStaticMethodID(env, integerClass, "parseInt", "(Ljava/lang/String;)I"); // 创建一个字符串对象,内容为"123" jstring str = (*env)->NewStringUTF(env, "123"); // 调用java.lang.Integer类的parseInt方法,将字符串转换为整数 jint num = (*env)->CallStaticIntMethod(env, integerClass, parseInt, str); // 检查是否发生了Java异常 jthrowable exception = (*env)->ExceptionOccurred(env); if (exception != NULL) { // 清除Java异常 (*env)->ExceptionClear(env); // 获取Java异常的原因对象 jthrowable cause = (*env)->ExceptionGetCause(env, exception); // 打印Java异常的堆栈跟踪信息 (*env)->ExceptionDescribe(env, exception); // 抛出新的Java异常,类型为java.lang.RuntimeException,信息为"JNI error" jclass runtimeExceptionClass = (*env)->FindClass(env, "java/lang/RuntimeException"); (*env)->ThrowNew(env, runtimeExceptionClass, "JNI error"); } else { // 打印转换后的整数 printf("The number is %d\n", num); }
4.3 完整例子
Chapter04.java
class Chapter04 : AppCompatActivity() { // 定义一些原生方法,用于调用测试函数 external fun testSystemCurrentTimeMillis(): Long // 原生方法:测试System.currentTimeMillis()函数的调用 external fun testIntegerParseInt(str: String): Int // 原生方法:测试Integer.parseInt()函数的调用 external fun testStringSubstring(str: String, beginIndex: Int, endIndex: Int): String // 原生方法:测试String.substring()函数的调用 external fun testArrayList(): Any // 原生方法:测试ArrayList的调用 var TAG = "Chapter04"; // 定义TAG变量用于日志输出 private lateinit var binding: Chapter04Binding // 在onCreate方法中调用原生方法,并打印结果 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) this.setTitle("Chapter04") binding = Chapter04Binding.inflate(layoutInflater) setContentView(binding.root) // Example of a call to a native method binding.sampleText.text = stringFromJNI() // 调用原生方法,返回字符串,并显示在UI中 // 调用测试函数并打印结果 val time = testSystemCurrentTimeMillis() // 调用原生方法获取当前时间戳 Log.d(TAG, "当前时间为:$time") try { val num = testIntegerParseInt("123A") // 调用原生方法尝试将字符串解析为整数(故意触发错误) // val num = testIntegerParseInt("123") // 调用原生方法将字符串解析为整数(正确示例) Log.d(TAG, "解析得到的数字为:$num") } catch (e: Exception) { e.printStackTrace() } val result = testStringSubstring("Hello JNI", 0, 5) // 调用原生方法截取字符串的子串 Log.d(TAG, "截取得到的子串为:$result") val firstElement = testArrayList() // 调用原生方法获取ArrayList的第一个元素 Log.d(TAG, "ArrayList的第一个元素为:$firstElement") } /** * 一个由本应用程序打包的 'jnidemo' native库实现的原生方法。 */ external fun stringFromJNI(): String // 原生方法:获取一个字符串 companion object { // 用于在应用程序启动时加载 'jnidemo' 库。 init { System.loadLibrary("jnidemo") } } }
chapter04.cpp
#include <jni.h> #include <string> using namespace chapter04; // 使用chapter04命名空间 // 定义一些常量,表示类名和方法签名 const char* SYSTEM_CLASS = "java/lang/System"; const char* INTEGER_CLASS = "java/lang/Integer"; const char* STRING_CLASS = "java/lang/String"; const char* ARRAYLIST_CLASS = "java/util/ArrayList"; const char* LONG_RETURN_VOID_ARG = "()J"; const char* INT_RETURN_STRING_ARG = "(Ljava/lang/String;)I"; const char* STRING_RETURN_INT_INT_ARG = "(II)Ljava/lang/String;"; const char* VOID_RETURN_VOID_ARG = "()V"; const char* BOOLEAN_RETURN_VOID_ARG = "()Z"; const char* BOOLEAN_RETURN_OBJECT_ARG = "(Ljava/lang/Object;)Z"; // 定义这个常量 const char* VOID_RETURN_OBJECT_ARG = "(Ljava/lang/Object;)V"; const char* OBJECT_RETURN_INT_ARG = "(I)Ljava/lang/Object;"; // 定义一个辅助函数,用于检查并打印JNI异常 void checkAndPrintException(JNIEnv *env) { jthrowable exception = env->ExceptionOccurred(); // 检查是否发生了Java异常 if (exception != NULL) { env->ExceptionDescribe(); // 打印Java异常的堆栈跟踪信息 } } // 定义一个测试函数,用于调用Java的System.currentTimeMillis()方法 jlong testSystemCurrentTimeMillis(JNIEnv *env) { // 查找System类 jclass systemClass = env->FindClass(SYSTEM_CLASS); checkAndPrintException(env); // 获取currentTimeMillis方法的ID jmethodID currentTimeMillisMethod = env->GetStaticMethodID(systemClass, "currentTimeMillis", LONG_RETURN_VOID_ARG); checkAndPrintException(env); // 调用currentTimeMillis方法,获取当前时间的毫秒数 jlong time = env->CallStaticLongMethod(systemClass, currentTimeMillisMethod); checkAndPrintException(env); // 返回时间值 return time; } // 定义一个测试函数,用于调用Java的Integer.parseInt()方法 jint testIntegerParseInt(JNIEnv *env, jstring str) { // 查找Integer类 jclass integerClass = env->FindClass(INTEGER_CLASS); checkAndPrintException(env); // 获取parseInt方法的ID jmethodID parseIntMethod = env->GetStaticMethodID(integerClass, "parseInt", INT_RETURN_STRING_ARG); checkAndPrintException(env); // 调用parseInt方法,将字符串转换为整数 jint num = env->CallStaticIntMethod(integerClass, parseIntMethod, str); checkAndPrintException(env); // 检查是否发生了Java异常 jthrowable exception = env->ExceptionOccurred(); if (exception != NULL) { // 清除Java异常 env->ExceptionClear(); // 获取Java异常的原因对象(如果API版本低于24,注释掉这一行) // jthrowable cause = env->ExceptionGetCause(exception); // 打印Java异常的堆栈跟踪信息 env->ExceptionDescribe(); // 修改函数调用,不传入任何参数 checkAndPrintException(env); // 抛出新的Java异常,类型为java.lang.RuntimeException,信息为"JNI error" jclass runtimeExceptionClass = env->FindClass("java/lang/RuntimeException"); env->ThrowNew(runtimeExceptionClass, "JNI error 》 testIntegerParseInt"); } // 返回转换后的整数 return num; } // 定义一个测试函数,用于调用Java的String.substring()方法 jstring testStringSubstring(JNIEnv *env, jstring str, jint beginIndex, jint endIndex) { // 查找String类 jclass stringClass = env->FindClass(STRING_CLASS); checkAndPrintException(env); // 获取substring方法的ID jmethodID substringMethod = env->GetMethodID(stringClass, "substring", STRING_RETURN_INT_INT_ARG); checkAndPrintException(env); // 调用substring方法,截取字符串的一部分 jstring result = (jstring) env->CallObjectMethod(str, substringMethod, beginIndex, endIndex); checkAndPrintException(env); // 检查是否发生了Java异常 jthrowable exception = env->ExceptionOccurred(); if (exception != NULL) { // 清除Java异常 env->ExceptionClear(); // 返回空字符串 result = env->NewStringUTF(""); } // 返回截取后的字符串 return result; } // 定义一个测试函数,用于创建一个Java的ArrayList对象,并向其中添加一些元素,然后获取其中的第一个元素 jobject testArrayList(JNIEnv *env) { // 查找ArrayList类 jclass arrayListClass = env->FindClass(ARRAYLIST_CLASS); checkAndPrintException(env); // 获取ArrayList类的构造方法ID,无参数,返回值为void jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "<init>", VOID_RETURN_VOID_ARG); checkAndPrintException(env); // 获取ArrayList类的add方法ID,参数为对象,返回值为布尔型 jmethodID arrayListAdd = env->GetMethodID(arrayListClass, "add", BOOLEAN_RETURN_OBJECT_ARG); checkAndPrintException(env); // 获取ArrayList类的get方法ID,参数为整型,返回值为对象 jmethodID arrayListGet = env->GetMethodID(arrayListClass, "get", OBJECT_RETURN_INT_ARG); checkAndPrintException(env); // 创建一个ArrayList对象 jobject arrayList = env->NewObject(arrayListClass, arrayListConstructor); checkAndPrintException(env); // 创建一些字符串对象,并向ArrayList中添加 jstring str1 = env->NewStringUTF("Hello"); jstring str2 = env->NewStringUTF("JNI"); jstring str3 = env->NewStringUTF("World"); env->CallBooleanMethod(arrayList, arrayListAdd, str1); env->CallBooleanMethod(arrayList, arrayListAdd, str2); env->CallBooleanMethod(arrayList, arrayListAdd, str3); checkAndPrintException(env); // 获取ArrayList中的第一个元素 jobject firstElement = env->CallObjectMethod(arrayList, arrayListGet, 0); checkAndPrintException(env); // 返回第一个元素 return firstElement; } extern "C" JNIEXPORT jstring JNICALL Java_com_ln28_jnidemo_Chapter04_stringFromJNI( // 修改函数名前缀,与Chapter04.kt中对应 JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } extern "C" JNIEXPORT jlong JNICALL Java_com_ln28_jnidemo_Chapter04_testSystemCurrentTimeMillis( // 修改函数名前缀,与Chapter04.kt中对应 JNIEnv* env, jobject /* this */) { return testSystemCurrentTimeMillis(env); } extern "C" JNIEXPORT jint JNICALL Java_com_ln28_jnidemo_Chapter04_testIntegerParseInt( // 修改函数名前缀,与Chapter04.kt中对应 JNIEnv* env, jobject /* this */, jstring str) { return testIntegerParseInt(env, str); } extern "C" JNIEXPORT jstring JNICALL Java_com_ln28_jnidemo_Chapter04_testStringSubstring( // 修改函数名前缀,与Chapter04.kt中对应 JNIEnv* env, jobject /* this */, jstring str, jint beginIndex, jint endIndex) { return testStringSubstring(env, str, beginIndex, endIndex); } extern "C" JNIEXPORT jobject JNICALL Java_com_ln28_jnidemo_Chapter04_testArrayList( // 修改函数名前缀,与Chapter04.kt中对应 JNIEnv* env, jobject /* this */) { return testArrayList(env); }
异常打印:
java.lang.NumberFormatException: For input string: "123A" java.lang.RuntimeException: JNI error 》 testIntegerParseInt 异常被捕获,程序没有崩溃。
4.4 疑问和问题解答
JNI接口方法表:
- JNI接口方法表是什么?它的作用是什么?
JNI接口方法表是一个结构体,其中包含了所有可用的JNI函数的指针。可以通过在C/C++代码中使用JNIEnv指针来调用这些函数。这个结构体为提供了一个与Java交互的接口,如访问和修改Java对象的字段,调用Java方法,处理Java异常等。 - 如何使用JNI接口方法表?
在C/C++代码中,可以通过JNIEnv指针来访问JNI接口方法表中的函数。例如,可以使用(*env)->GetObjectField(env, obj, fieldID)
来访问一个Java对象的字段。 - JNI接口方法表中的函数有哪些?我需要了解所有的函数吗?
JNI接口方法表中包含了大约100多个函数,这些函数提供了丰富的功能,如创建Java对象,访问和修改Java对象的字段,调用Java方法,抛出和处理Java异常等。不需要了解所有的函数,只需要掌握常用的一些函数就可以了。 - 有没有一个示例展示如何使用JNI接口方法表的某个函数?
当然,例如,下面的代码展示了如何使用JNI接口方法表的GetObjectField
函数和SetIntField
函数来访问和修改Java对象的字段:
jclass cls = (*env)->GetObjectClass(env, obj); jfieldID fieldID = (*env)->GetFieldID(env, cls, "intValue", "I"); jint value = (*env)->GetIntField(env, obj, fieldID); value++; (*env)->SetIntField(env, obj, fieldID, value);
5.我是否可以修改JNI接口方法表?
不,不能修改JNI接口方法表。这个结构体是由JNI环境提供的,只能通过JNIEnv指针来访问它,不能修改它。
JNI基础API:
- JNI基础API包含哪些函数?
JNI基础API包含了许多函数,如创建Java对象,访问和修改Java对象的字段,调用Java方法,抛出和处理Java异常等。一些常用的函数包括NewObject
,GetObjectClass
,GetFieldID
,GetObjectField
,SetObjectField
,CallVoidMethod
等。 - JNI基础API的主要作用是什么?
JNI基础API的主要作用是提供一个接口,让可以在C/C++代码中与Java交互。可以通过JNI基础API来创建Java对象,访问和修改Java对象的字段,调用Java方法,抛出和处理Java异常等。 - 如何使用JNI基础API访问和修改Java对象的字段?
可以使用GetFieldID
函数来获取一个字段的ID,然后使用Get<Type>Field
和Set<Type>Field
函数来访问和修改该字段。例如:
jclass cls = (*env)->GetObjectClass(env, obj);
jfieldID fieldID = (*env)->GetFieldID(env, cls, "intValue", "I");
jint value = (*env)->GetIntField(env, obj, fieldID);
value++;
(*env)->SetIntField(env, obj, fieldID, value);
4.如何使用JNI基础API调用Java方法?
可以使用GetMethodID
或者GetStaticMethodID
函数来获取一个方法的ID,然后使用Call<Type>Method
或者CallStatic<Type>Method
函数来调用该方法。例如:
jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID mid = (*env)->GetMethodID(env, cls, "print", "(I)V");
(*env)->CallVoidMethod(env, obj, mid, 123);
5.如何创建和处理Java对象?
可以使用NewObject
函数来创建一个新的Java对象,然后使用各种Get<Type>Field
和Set<Type>Field
函数来访问和修改其字段。如果需要处理Java对象的数组,可以使用NewObjectArray
,GetObjectArrayElement
,SetObjectArrayElement
等函数。
JNI异常API:
- JNI异常API是用来做什么的?
JNI异常API用于在native代码中抛出和处理Java异常。当在C/C++代码中调用Java方法时,可能会发生Java异常,可以使用JNI异常API来检查和处理这些异常。2 - 如果在native方法中发生了Java异常,我应该如何处理?可以使用
ExceptionOccurred
函数来检查是否发生了异常,然后使用ExceptionDescribe
函数来打印异常的堆栈轨迹,使用ExceptionClear
函数来清除异常。如果想在native代码中处理异常,可以使用ThrowNew
函数来抛出一个新的异常。
jmethodID mid = (*env)->GetMethodID(env, cls, "method", "()V"); if ((*env)->ExceptionOccurred(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); return; }
3.我应该如何使用JNI异常API抛出一个新的Java异常?
可以使用ThrowNew
函数来抛出一个新的Java异常。需要指定异常类和异常信息。
jclass exCls = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); if (exCls != NULL) { (*env)->ThrowNew(env, exCls, "Illegal argument"); }
4.JNI异常API中的ExceptionCheck
和ExceptionOccurred
有什么区别?
ExceptionCheck
和ExceptionOccurred
函数都可以用来检查是否发生了Java异常,但是ExceptionCheck
函数不会清除异常状态,而ExceptionOccurred
函数会返回一个异常对象并清除异常状态。
5。如果我忽略了一个Java异常(没有清除或者抛出),会发生什么?
如果在native方法中忽略了一个Java异常,这个方法将会立即返回,不会执行后面的代码。如果这个native方法是由Java调用的,Java将会收到这个异常。所以,如果不打算处理Java异常,应该至少使用ExceptionDescribe
函数来打印异常的堆栈轨迹,然后使用ExceptionClear
函数来清除异常,避免影响后续代码的执行。
(1) JNI 提示 | Android NDK | Android Developers - Android 开发者. JNI 提示 | Android NDK | Android Developers.
(2) NDK系列:JNI基础 - 掘金. NDK系列:JNI基础 - 掘金.