本地代码中如何缓存和抛出异常
根据一个例子来介绍:
1.新建一个CatchThrow.java
public class CatchThrow {
public native void doit() throws IllegalArgumentException;
private void callback() throws NullPointerException{
throw new NullPointerException("CatchThrow.callback");
}
}
2.JNI实现
JNIEXPORT void JNICALL Java_com_test_git_jnidemo_JniUtil_CatchThrow_doit
(JNIEnv *env, jobject jobj){
jthrowable exc;
//获取jclass
jclass cls = (*env).GetObjectClass(jobj);
//获取callback 方法id
jmethodID mid = (*env).GetMethodID(cls, "callback", "()V");
if(mid == NULL){
return;
}
//调用callback方法
(*env).CallVoidMethod(jobj, mid);
//异常检查
exc = (*env).ExceptionOccurred();
if(exc){
//异常处理
jclass newExcCls;
//输出异常
(*env).ExceptionDescribe();
//清除异常
(*env).ExceptionClear();
newExcCls = (*env).FindClass("java/lang/IllegalArgumentException");
if(newExcCls == NULL){
//没有发现异常
return;
}
(*env).ThrowNew(newExcCls, "thrown from C code");
}
};
3.Java中调用
CatchThrow catchThrow = new CatchThrow();
try {
catchThrow.doit();
}catch (Exception e){
Log.i(TAG, "In Java catchThrow: " + e);
}
4.输出结果:
09-29 17:16:16.291 18958-18958/com.test.git.jnidemo W/System.err: java.lang.NullPointerException: CatchThrow.callback
09-29 17:16:16.291 18958-18958/com.test.git.jnidemo W/System.err: at com.test.git.jnidemo.JniUtil.CatchThrow.callback(CatchThrow.java:11)
09-29 17:16:16.291 18958-18958/com.test.git.jnidemo W/System.err: at com.test.git.jnidemo.JniUtil.CatchThrow.doit(Native Method)
09-29 17:16:16.291 18958-18958/com.test.git.jnidemo W/System.err: at com.test.git.jnidemo.Activity.MainActivity.catchThrow(MainActivity.java:26)
09-29 17:16:16.291 18958-18958/com.test.git.jnidemo W/System.err: at com.test.git.jnidemo.Activity.MainActivity.onCreate(MainActivity.java:19)
......
09-29 17:16:16.301 18958-18958/com.test.git.jnidemo I/MainActivity-: In Java catchThrow: java.lang.IllegalArgumentException: thrown from C code
制作一个抛出异常的工具函数
抛出一个异常通常需要两步:
1.通过FindClass找到异常类
2.调用ThrowNew函数生成异常。
void JUN_ThrowByName(JNIEnv *env, const char *name, const char *msg){
//生成jclass
jclass cls = (*env).FindClass(name);
//如果cls为NULL,一个异常已经发生
if(cls != NULL){
(*env).ThrowNew(cls, msg);
}
//释放本地引用
(*env).DeleteLocalRef(cls);
};
JNU_ThrowByName 这个工具函数首先使用 FindClass 函数来找到异常类,如果 FindClass 执行失败(返回 NULL),VM 会抛出一个异常(比如 NowClassDefFoundError),这种情况下 JNI_ThrowByName 不会再抛出另外一个
异常。如果 FindClass 执行成功的话,我们就通过 ThrowNew 来抛出一个指定名 字的异常。当函数 JNU_ThrowByName 返回时,它会保证有一个异常需要处理,但 这个异常不一定是 name 参数指定的异常。当函数返回时,记得要删除指向异常 类的局部引用。向 DeleteLocalRef 传递 NULL 不会产生作用。
异常处理
异常检查
检查异常有两种方式:
1.大部分JNI函数会通过特定的返回值(NULL)来表示已经发生了一个错误,并且当前线程中又一个异常需要处理。在C语言中,用返回值来标识错误信息是一个很常见的方式。
jclass localRefCls = (*env).FindClass("java/lang/String");
if(localRefCls == NULL){
return; //exeption
}
2.当一个JNI函数返回一个明确的错误码时,可以用ExceptionCheck来检查是否有异常发生。但是,用返回的错误码来判断比较高效。一旦JNI函数的返回值是一个错误码,那么接下来调用ExceptionCheck肯定会返回JNI_TRUE。
//异常检查
exc = (*env).ExceptionOccurred();
jboolean error = (*env).ExceptionCheck();
if(error){
//异常处理
jclass newExcCls;
//输出异常
(*env).ExceptionDescribe();
//清除异常
(*env).ExceptionClear();
newExcCls = (*env).FindClass("java/lang/IllegalArgumentException");
if(newExcCls == NULL){
//没有发现异常
return;
}
(*env).ThrowNew(newExcCls, "thrown from C code");
}
异常处理
本地代码通常有两种方式来处理一个异常:
1.一旦发生异常,立即返回,让调用者处理这个异常。
2.通过ExceptionClear清除异常,然后执行自己的异常处理代码。
当一个异常发生后,必须先检查、处理、清除异常后再做其他JNI函数调用,否则的话,结果未知。
通常来说,当有一个未处理的异常时,你只可以调用两种JNI函数:异常处理和清除VM资源的函数。
当异常发生时,释放资源是一件很重要的事。比如:
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
{
const jchar *cstr = (*env)->GetStringChars(env, jstr);
if (c_str == NULL) {
return;
}
...
if (...) { /* exception occurred */
(*env)->ReleaseStringChars(env, jstr, cstr);
return;
}
...
/* normal return */
(*env)->ReleaseStringChars(env, jstr, cstr);
}
工具函数中的异常
编写工具函数时,要把工具函数内部分发生的异常传播到调用它的方法中。
这里有两个方法:
1.对调用者来说,工具函数提供一个错误返回码比简单地把异常传播过去更方便一些。
2.工具函数在发生异常时尤其需要注意管理局部引用的方式。
jvalue JUN_CallMethodByName(JNIEnv *env,
jboolean *hasException,
jobject jobj,
const char* name,
const char* descriptor,
...){
va_list args;
jclass clazz;
jmethodID mid;
jvalue result;
if((*env).EnsureLocalCapacity(2) == JNI_OK){
clazz = (*env).GetObjectClass(jobj);
mid = (*env).GetMethodID(clazz, name, descriptor);
if(mid){
const char *p = descriptor;
while (*p != ')') p++;
p++;
va_start(args, descriptor);
switch (*p){
case 'V':
(*env).CallVoidMethodV(jobj, mid, args);
break;
case '[':
case 'L':
result.l = (*env).CallObjectMethodV(jobj, mid, args);
break;
case 'Z':
result.z = (*env).CallBooleanMethodV(jobj, mid, args);
break;
default:
(*env).FatalError("illegal descriptor");
break;
}
va_end(args);
}
(*env).DeleteLocalRef(clazz);
}
if(hasException){
*hasException = (*env).ExceptionCheck();
}
return result;
};
JNU_CallMethodByName 的参数当中有一个 jboolean 指针,如果函数执行成功的 话,指针指向的值会被设置为 JNI_TRUE,如果有异常发生的话,会被设置成 JNI_FALSE。这就可以让调用者方便地检查异常。
JNU_CallMethodByName 首先通过 EnsureLocalCapacity 来确保可以创建两个局 部引用,一个类引用,一个返回值。接下来,它从对象中获取类引用并查找方法 ID。根据返回类型,switch 语句调用相应的 JNI 方法调用函数。回调过程完成 后,如果 hasException 不是 NULL,我们调用 ExceptionCheck 检查异常。 函数 ExceptionCheck 和 ExceptionOccurred 非常相似,不同的地方是,当有异 常发生时,ExceptionCheck 不会返回一个指向异常对象的引用,而是返回 JNI_TRUE,没有异常时,返回 JNI_FALSE。而 ExceptionCheck 这个函数不会返 回一个指向异常对象的引用,它只简单地告诉 地代码是否有异常发生。上面的 代码如果使用 ExceptionOccurred 的话,应该这么写:
if(hasException){
// *hasException = (*env).ExceptionCheck();
jthrowable exc = (*env).ExceptionOccurred();
*hasException = exc != NULL;
(*env).DeleteLocalRef(exc);
}
为了删除指向异常对象的局部引用,DeleteLocalRef 方法必须被调用。 使用 JNU_CallMethodByName 这个工具函数,我们可以重写 Instance-MethodCall.nativeMethod 方法的实现:
JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj) {
printf("In C\n");
JNU_CallMethodByName(env, NULL, obj, "callback", "()V"); }
调用 JNU_CallMethodByName 函数后,我们不需要检查异常,因为 地方法后面 会立即返回。