JNI异常

1,485 阅读4分钟

JNI 异常概述

JNI没有异常处理机制,也就是没有Java那一套 try-catch-finally。这样做,一方面是为性能,另一方面是因为在某些情况下,没有足够的运行时类型信息来提供异常检测。因此我们有责任保证不使用空指针,不传入不合法参数。

然而,JNI提供了一系列的函数,用于检测是否有异常发生、抛出异常、清空异常。

处理异常

一般JNI函数会通过返回值和抛出异常来报告异常情况。因此通常我们可以通过返回值来快速判断函数是否有异常发生。然而有些情况下,我们必须首先检测异常,因为我们无法得到返回值,这些情况有两种,如下

  1. 在native函数中调用Java方法,而Java方法可能会抛出异常。此时无法通过返回值判断是否有异常发生,只有检测是否发生了异常。
  2. 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源码中,有很多异常处理的代码,非常值得我们在阅读时进行学习。

参考

docs.oracle.com/javase/7/do…

docs.oracle.com/javase/7/do…