JNI 异常概述
JNI没有异常处理机制,也就是没有Java那一套 try-catch-finally。这样做,一方面是为性能,另一方面是因为在某些情况下,没有足够的运行时类型信息来提供异常检测。因此我们有责任保证不使用空指针,不传入不合法参数。
然而,JNI提供了一系列的函数,用于检测是否有异常发生、抛出异常、清空异常。
处理异常
一般JNI函数会通过返回值和抛出异常来报告异常情况。因此通常我们可以通过返回值来快速判断函数是否有异常发生。然而有些情况下,我们必须首先检测异常,因为我们无法得到返回值,这些情况有两种,如下
- 在native函数中调用Java方法,而Java方法可能会抛出异常。此时无法通过返回值判断是否有异常发生,只有检测是否发生了异常。
- JNI的数组操作不会返回一个错误的返回值,而是会抛出ArrayIndexOutOfBoundsException或ArrayStoreException。因此也不能使用返回值来判断是否发生了异常,只有进行异常检测。
说了这么多,我们如何检测异常呢,有下面两种方法
jthrowable ExceptionOccurred(JNIEnv *env);
jboolean ExceptionCheck(JNIEnv *env);
这两个函数都可以用来检测是否发生了异常,而它们的区别在于,ExceptionOccurred 函数会返回一个异常引用,而 ExceptionCheck 返回一个布尔值,用来表明是否有异常发生而已。
现在我们来用一个native函数调用Java方法为例,来解释如何检测异常。
现在这段native函数代码调用了java层的方法
JNIEXPORT void JNICALL
Java_com_example_helllojni_MainActivity_helloFromJava(JNIEnv *env, jobject thiz, jobject person) {
jclass activity_clazz = env->GetObjectClass(thiz);
jmethodID methodID_setAgeFromJni = env->GetMethodID(activity_clazz, "setAgeFromJni", "(I)Z");
// 1. 通过Java层方法来检测并设置年龄
env->CallBooleanMethod(thiz, methodID_setAgeFromJni, 1);
// 2. 如果Java层判断年龄不合法会抛出异常,这里需要检测处理,否则程序挂掉
if (env->ExceptionCheck()) {
// 首先必须要清除异常,再调用其它函数进行异常补救
env->ExceptionClear();
return;
}
// 3. 走到这里说明刚才设置年龄的操作合法了,现在来设置名字
jclass person_clazz = env->GetObjectClass(person);
jmethodID methodID_setName = env->GetMethodID(person_clazz, "setName", "(Ljava/lang/String;)V");
jstring name = env->NewStringUTF("David");
env->CallVoidMethod(person, methodID_setName, name);
}
第一步调用了Java层的方法来检测年龄是否合法,如何合法就设置年龄。Java层的代码如下
public boolean setAgeFromJni(int age) {
if (age < 0 || age > 200) {
throw new IllegalArgumentException("Age must be in [0, 200]");
}
mPerson.setAge(age);
return true;
}
虽然这个Java方法返回了一个boolean值,但是一旦抛出异常,是无法通过这个返回值检测异常的,因此只能在native方法通过检测这个异常来判断Java层方法是否执行成功。
而如果Java层抛出了异常,而native层函数没有处理这个异常,那么程序会挂掉。刚才的代码中,只是简单通过 ExceptionClear() 清除异常,并直接返回。当然还可以有其它处理方案,例如重新设置一个合法的年龄,但是前提还是要先清除这个异常。
在native函数中检测到异常后,只有少数几个函数可以调用,其中包括检测异常函数,但是请记住,无论你如何处理异常,首先调用 ExceptionClear() 把这个异常给清除了,之后再调用其它函数。
抛出异常
JNI 层能够处理自己的异常,例如操作数组发生的异常,也能处理Java层抛出的异常,例如调用Java方法而抛出的异常。其实JNI也能抛出一个Java异常让Java方法处理。
抛出Java异常的函数如下
// 下面两个函数,正常情况下返回0,否则返回负值
jint Throw(JNIEnv *env, jthrowable obj);
jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);
这两个函数都可以抛出异常,但是比较之后可以发现,第二个函数更方便,因此第一个函数要构造一个Java异常对象,这个过程复杂。
例如在Java层调用一个native方法,代码如下
extern "C"
JNIEXPORT void JNICALL
Java_com_example_helllojni_MainActivity_checkAge(JNIEnv *env, jobject thiz, jint age) {
if (age < 0 || age > 100) {
jclass ex_clazz = env->FindClass("java/lang/IllegalArgumentException");
env->ThrowNew(ex_clazz, "Age range [0, 100]!!!");
}
}
这个native方法,检测年龄值如果不合法,抛出了一个 IllegalArgumentException。而在Java层,调用这个方法时,必须要处理这个异常的,否则程序披挂,代码如下
try {
checkAge(-1);
} catch (Exception e) {
Log.d("david", "catch ex = " + e.getMessage());
}
工作感想
异常检测与处理,在JNI开发中是十分重要的一环,大家千万不可忽视,否则我们很难找出JNI层的Bug在哪里。Android的JNI源码中,有很多异常处理的代码,非常值得我们在阅读时进行学习。