JNI/NDK开发指南(十一)——JNI异常处理
最后更新于:2022-04-01 07:39:12
### 异常简介
异常,显而意见就是程序在运行期间没有按照正常的程序逻辑执行,在执行过程当中出现了某种错误,导致程序崩溃。在Java中异常分为运行时异常(RuntimeException)和编译时异常,在程序中有可能运行期间发生异常的逻辑我们会用try…catch…来处理,如果没有处理的话,在运行期间发生异常就会导致程序奔溃。而编译时异常是在编译期间就必须处理的。本章主要介绍运行时异常。
**示例1:**
~~~
// 运行时异常
public static void exceptionCallback() {
int a = 20 / 0;
System.out.println("--->" + a);
}
~~~
**示例2:**
~~~
// 编译期间异常
public static void testException() throws Exception {
// ...
System.out.println("testException() invoked!");
}
public static void main(String[] args) {
exceptionCallback();
try {
testException();
} catch (Exception e) {
e.printStackTrace();
}
// ....
}
~~~
~~~
在示例2中,testException方法声明时显示抛出了一个java.lang.Exception异常,所以在程序调用的地方必须用try...catch处理。
~~~
大家都知道,如果示例2中main方法执行到调用exceptionCallback方法时,方法第一行有一个除0的操作,因此该方法会抛出`java.lang.ArithmeticException`数学异常,而在main方法调用的时候并没有处理这个函数在运行时可能会发生的异常,所以会导致程序立即结束,而后面的代码`try{testException();}catch(Exception e) {e.printStackTrace();}`都不会被执行。运行示例2程序的你会看到下面的结果:
~~~
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.study.jnilearn.JNIException.exceptionCallback(JNIException.java:8)
at com.study.jnilearn.JNIException.main(JNIException.java:22)
~~~
我们改进一下上面这个程序:
~~~
public static void main(String[] args) {
try {
exceptionCallback();
} catch (Exception e) {
e.printStackTrace();
}
try {
testException();
} catch (Exception e) {
e.printStackTrace();
}
}
~~~
这时我们运行程序,调用`exceptionCallback`方法时会引发`java.lang.ArithmeticException: / by zero`异常,由于我们用try…catch块显示处理了异常,所以程序会继续往下执行,调用testException()函数,打印`testException() invoked!`。运行结果如下所示:
~~~
java.lang.ArithmeticException: / by zero
at com.study.jnilearn.JNIException.exceptionCallback(JNIException.java:8)
at com.study.jnilearn.JNIException.main(JNIException.java:24)
testException() invoked!
~~~
### Java与JNI处理异常的区别
下面来小结一下:
1、在Java中如果觉得某段逻辑可能会引发异常,用try…catch机制来捕获并处理异常即可
2、如果在Java中发生运行时异常,没有使用try…catch来捕获,会导致程序直接奔溃退出,**后续的代码都不会被执行**
3、编译时异常,是在方法声明时显示用throw声明了某一个异常,编译器要求在调用的时候必须显示捕获处理
`public static void testException() throws Exception {}`
上面这几点,写过Java的朋友都知道,而且很简单,但我为什么还要拿出来说呢,其实我想重点说明的是,在JNI中发生的异常和Java完全不一样。***我们在写JNI程序的时候,JNI没有像Java一样有try…catch…final这样的异常处理机制,面且在本地代码中调用某个JNI接口时如果发生了异常,后续的本地代码不会立即停止执行,而会继续往下执行后面的代码。***
### 异常处理示例
**示例3:**
这个例子在main中调用了doit本地方法,在本地方法中会回调`exceptionCallback`方法,该方法中会引发一个除0的运行时异常`java.lang.ArithmeticException`,我们通过这个示例来学习在JNI中如何来正确处理这种异常。
~~~
package com.study.jnilearn;
public class JNIException {
public static native void doit();
public static void exceptionCallback() {
int a = 20 / 0;
System.out.println("--->" + a);
}
public static void normalCallback() {
System.out.println("In Java: invoke normalCallback.");
}
public static void main(String[] args) {
doit();
}
static {
System.loadLibrary("JNIException");
}
}
~~~
~~~
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_study_jnilearn_JNIException */
#ifndef _Included_com_study_jnilearn_JNIException
#define _Included_com_study_jnilearn_JNIException
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_study_jnilearn_JNIException
* Method: doit
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_study_jnilearn_JNIException_doit
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
// JNIException.c
#include "com_study_jnilearn_JNIException.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_com_study_jnilearn_JNIException_doit(JNIEnv *env, jclass cls) {
jthrowable exc = NULL;
jmethodID mid = (*env)->GetStaticMethodID(env,cls,"exceptionCallback","()V");
if (mid != NULL) {
(*env)->CallStaticVoidMethod(env,cls,mid);
}
printf("In C: Java_com_study_jnilearn_JNIException_doit-->called!!!!");
if ((*env)->ExceptionCheck(env)) { // 检查JNI调用是否有引发异常
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env); // 清除引发的异常,在Java层不会打印异常的堆栈信息
(*env)->ThrowNew(env,(*env)->FindClass(env,"java/lang/Exception"),"JNI抛出的异常!");
//return;
}
mid = (*env)->GetStaticMethodID(env,cls,"normalCallback","()V");
if (mid != NULL) {
(*env)->CallStaticVoidMethod(env,cls,mid);
}
}
~~~
程序运行结果如下:
~~~
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.study.jnilearn.JNIException.exceptionCallback(JNIException.java:8)
at com.study.jnilearn.JNIException.doit(Native Method)
at com.study.jnilearn.JNIException.main(JNIException.java:17)
Exception in thread "main" java.lang.Exception: JNI抛出的异常!
at com.study.jnilearn.JNIException.doit(Native Method)
In Java: invoke normalCallback.
at com.study.jnilearn.JNIException.main(JNIException.java:17)
In C: Java_com_study_jnilearn_JNIException_doit-->called!!!!
~~~
在Main方法中调用doit本地方法后,程序的控制权即交给了JNI,在doit的本地方法中回调exceptionCallback方法,引发了一个`java.lang.ArithmeticException`异常,但本地接口并不会马上退出,而是会继续执行后面的代码,所以我们在调用完一个任何一个JNI接口之后,必须要做的一件事情就是检查这次JNI调用是否发生了异常,如果发生了异常不处理,而继续让程序执行后面的逻辑,将会产生不可预知的后果。在本例中,我们调用了JNI的`ExceptionCheck`函数检查最近一次JNi调用是否发生了异常,如果有异常这个函数返回JNI_TRUE,否则返回JNI_FALSE。当检测到异常时,我们调用`ExceptionDescribe`函数打印这个异常的堆栈信息,然后再调用`ExceptionClear`函数清除异常堆栈信息的缓冲区(如果不清除,后面调用ThrowNew抛出的异常堆栈信息会覆盖前面的异常信息),最后调用`ThrowNew`函数手动抛出一个java.lang.Exception异常。但在JNI中抛出未捕获的异常与Java的异常处理机制不一样,在JNI中并不会立即终止本地方法的执行,而是继续执行后面的代码。这种情况需要我们手动来处理。在例中的38行,如果你不用return马上退出方法的话,37行ThrowNew后面的代码依然会继续执行,如程序运行的结果一样,仍然会回调`normalCallback`方法,打印出:invoke normalCallback.
异常检查JNI还提供了另外一个接口,`ExceptionOccurred`,如果检测有异常发生时,该函数会返回一个指向当前异常的引用。作用和`ExceptionCheck`一样,两者的区别在于返回值不一样。我们改造一下上面的例子:
~~~
// ....
jthrowable exc = NULL;
exc = (*env)->ExceptionOccurred(env); // 返回一个指向当前异常对象的引用
if (exc) {
(*env)->ExceptionDescribe(env); // 打印Java层抛出的异常堆栈信息
(*env)->ExceptionClear(env); // 清除异常信息
// 抛出我们自己的异常处理
jclass newExcCls;
newExcCls = (*env)->FindClass(env,"java/lang/Exception");
if (newExcCls == NULL) {
return;
}
(*env)->ThrowNew(env, newExcCls, "throw from C Code.");
}
// ....
~~~
### 写一个抛出异常的工具类
当需要抛出自己的异常处理逻辑时,需要二步,调用FindClass找到异常处理类,然后调用ThrowNew抛出一个异常。为了简化操作步聚,我们写一个工具函数,根据一个异常类名专门用来生成一个指定名字的异常:
~~~
void JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg)
{
// 查找异常类
jclass cls = (*env)->FindClass(env, name);
/* 如果这个异常类没有找到,VM会抛出一个NowClassDefFoundError异常 */
if (cls != NULL) {
(*env)->ThrowNew(env, cls, msg); // 抛出指定名字的异常
}
/* 释放局部引用 */
(*env)->DeleteLocalRef(env, cls);
}
~~~
### 异常发生后释放资源
在异常发生后,释放资源是一件很重要的事情。下面的例子中,调用 GetStringChars 函数后,如果后面的代码发生异常,要记得调用 ReleaseStringChars 释放资源。
~~~
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 ((*env)->ExceptionCheck(env)) { /* 异常检查 */
(*env)->ReleaseStringChars(env, jstr, cstr); // 发生异常后释放前面所分配的内存
return;
}
...
/* 正常返回 */
(*env)->ReleaseStringChars(env, jstr, cstr);
}
~~~
### 总结
1、当调用一个JNI函数后,必须先检查、处理、清除异常后再做其它 JNI 函数调用,否则会产生不可预知的结果。
2、一旦发生异常,立即返回,让调用者处理这个异常。或 调用 ExceptionClear 清除异常,然后执行自己的异常处理代码。
3、异常处理的相关JNI函数总结:
1> ExceptionCheck:检查是否发生了异常,若有异常返回JNI_TRUE,否则返回JNI_FALSE
2> ExceptionOccurred:检查是否发生了异常,若用异常返回该异常的引用,否则返回NULL
3> ExceptionDescribe:打印异常的堆栈信息
4> ExceptionClear:清除异常堆栈信息
5> ThrowNew:在当前线程触发一个异常,并自定义输出异常信息
`jint (JNICALL *ThrowNew) (JNIEnv *env, jclass clazz, const char *msg);`
6> Throw:丢弃一个现有的异常对象,在当前线程触发一个新的异常
`jint (JNICALL *Throw) (JNIEnv *env, jthrowable obj);`
7> FatalError:致命异常,用于输出一个异常信息,并终止当前VM实例(即退出程序)
`void (JNICALL *FatalError) (JNIEnv *env, const char *msg);`