JNI/NDK開發指南(九)---JNI異常處理

異常簡介

異常,顯而意見就是程序在運行期間沒有按照正常的程序邏輯執行,在執行過程當中出現了某種錯誤,導致程序崩潰。在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);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章