JNI由淺入深_9_JNI 異常處理

1 、本地代碼中如何緩存和拋出異常

下面的代碼中演示瞭如何聲明一個會拋出異常的本地方法。CatchThrow這個類聲明瞭一個會拋出IllegalArgumentException異常的名叫doit的本地方法。
<span style="font-family:Comic Sans MS;font-size:14px;">class CatchThrow {
     private native void doit()
         throws IllegalArgumentException;
     private void callback() throwsNullPointerException {
         throw newNullPointerException("CatchThrow.callback");
     }
 
     public static void main(String args[]) {
         CatchThrow c = new CatchThrow();
         try {
             c.doit();
         } catch (Exception e) {
             System.out.println("InJava:\n\t" + e);
         }
     }
     static {
        System.loadLibrary("CatchThrow");
     }
 }</span>
Main方法調用本地方法doit,doit方法的實現以下:
<span style="font-family:Comic Sans MS;font-size:14px;">JNIEXPORT void JNICALL
 Java_CatchThrow_doit(JNIEnv*env, jobject obj)
 {
     jthrowable exc;
     jclass cls = (*env)->GetObjectClass(env,obj);
     jmethodID mid =
         (*env)->GetMethodID(env, cls,"callback", "()V");
     if (mid == NULL) {
         return;
     }
     (*env)->CallVoidMethod(env, obj, mid);
     exc = (*env)->ExceptionOccurred(env);
     if (exc) {
         /* We don't do much with the exception,except that
            we print a debug message for it,clear it, and
            throw a new exception. */
         jclass newExcCls;
         (*env)->ExceptionDescribe(env);
         (*env)->ExceptionClear(env);
         newExcCls = (*env)->FindClass(env,
                       "java/lang/IllegalArgumentException");
         if (newExcCls == NULL) {
             /* Unable to find the exceptionclass, give up. */
             return;
         }
         (*env)->ThrowNew(env, newExcCls,"thrown from C code");
     }
 }</span>

運行程序,輸出是:
java.lang.NullPointerException:
         at CatchThrow.callback(CatchThrow.java)
         at CatchThrow.doit(Native Method)
         at CatchThrow.main(CatchThrow.java)
 In Java:
         java.lang.IllegalArgumentException:thrown from C code
回調方法拋出一個NullPointerException異常。當CallVoidMethod把控制權交給本地方法時,本地代碼會經過ExceptionOccurred來檢查這個異常。在咱們的例子中,當一個異常被檢測到時,本地代碼經過調用ExceptionDescribe來輸出一個關於這個異常的描述信息,而後經過調用ExceptionClear清除異常信息,最後,拋出一個IllegalArgumentException。
和JAVA中的異常機制不同,JNI拋出的異常(例如,經過ThrowNew方法)不被處理的話,不會當即終止本地方法的執行。異常發生後,JNI程序員必須手動處理。
 

1.1 製做一個拋出異常的工具函數

 
拋出異常一般須要兩步:經過FindClass找到異常類、調用ThrowNew函數生成異常。爲了簡化這個過程,咱們寫了一個工具函數專門用來生成一個指定名字的異常。
<span style="font-family:Comic Sans MS;font-size:14px;">void
 JNU_ThrowByName(JNIEnv *env, const char *name,const char *msg)
 {
    jclass cls = (*env)->FindClass(env, name);
    /* if cls is NULL, an exception has already been thrown */
    if (cls != NULL) {
         (*env)->ThrowNew(env, cls, msg);
    }
     /* free the local ref */
     (*env)->DeleteLocalRef(env, cls);
 }</span>


本書中,若是一個函數有JNU前綴的話,意味它是一個工具函數。JNU_ThrowByName這個工具函數首先使用FindClass函數來找到異常類,若是FindClass執行失敗(返回NULL),VM會拋出一個異常(好比NowClassDefFoundError),這種狀況下JNI_ThrowByName不會再拋出另一個異常。若是FindClass執行成功的話,咱們就經過ThrowNew來拋出一個指定名字的異常。當函數JNU_ThrowByName返回時,它會保證有一個異常須要處理,但這個異常不必定是name參數指定的異常。當函數返回時,記得要刪除指向異常類的局部引用。向DeleteLocalRef傳遞NULL不會產生做用。

2 妥善地處理異常

JNI程序員必須可以預測到可能會發生異常的地方,並編寫代碼進行檢查。妥善地異常處理有時很繁鎖,可是一個高質量的程序不可或缺的。

2.1 異常檢查

檢查一個異常是否發生有兩種方式。
第一種方式是:大部分JNI函數會經過特定的返回值(好比NULL)來表示已經發生了一個錯誤,而且當前線程中有一個異常須要處理。在C語言中,用返回值來標識錯誤信息是一個很常見的方式。下面的例子中演示瞭如何經過GetFieldID的返回值來檢查錯誤。這個例子包含兩部分,定義了一些實例字段(handle、length、width)的類Window和一個緩存這些字段的字段ID的本地方法。雖然這些字段位於Window類中,調用GetFieldID時,咱們仍然須要檢查是否有錯誤發生,由於VM可能沒有足夠的內存分配給字段ID。
 
<span style="font-family:Comic Sans MS;font-size:14px;"> /* a class in the Java programming language */
    public class Window {
        long handle;
        int length;
        int width;
        static native void initIDs();
        static {
            initIDs();
        }
  }
  
   /* C codethat implements Window.initIDs */
   jfieldID FID_Window_handle;
   jfieldID FID_Window_length;
   jfieldID FID_Window_width;
 
   JNIEXPORT void JNICALL
   Java_Window_initIDs(JNIEnv *env, jclass classWindow)
   {
       FID_Window_handle =
           (*env)->GetFieldID(env, classWindow,"handle", "J");
       if (FID_Window_handle == NULL) {  /* important check. */
          return; /* erroroccurred. */
       }
      FID_Window_length =
          (*env)->GetFieldID(env, classWindow,"length", "I");
      if (FID_Window_length == NULL) {  /* important check. */
         return; /* erroroccurred. */
       }
       FID_Window_width =
          (*env)->GetFieldID(env, classWindow,"width", "I");
       /* no checks necessary; weare about to return anyway */
  }</span>

第二種方式:
<span style="font-family:Comic Sans MS;font-size:14px;">public class Fraction {
     // details such as constructors omitted
     int over, under;
     public int floor() {
         return Math.floor((double)over/under);
     }
 }
/* Native code that callsFraction.floor. Assume method ID
    MID_Fraction_floor has been initializedelsewhere. */
 void f(JNIEnv*env, jobject fraction)
 {
    jint floor = (*env)->CallIntMethod(env, fraction,
                                       MID_Fraction_floor);
     /* important: check if an exception wasraised */
     if ((*env)->ExceptionCheck(env)) {
         return;
     }
     ... /* use floor */
 }</span>

當一個JNI函數返回一個明確的錯誤碼時,你仍然能夠用ExceptionCheck來檢查是否有異常發生。可是,用返回的錯誤碼來判斷比較高效。一旦JNI函數的返回值是一個錯誤碼,那麼接下來調用ExceptionCheck確定會返回JNI_TRUE。

2.2 異常處理

 
本地代碼一般有兩種方式來處理一個異常:
一、一旦發生異常,當即返回,讓調用者處理這個異常。
二、經過ExceptionClear清除異常,而後執行本身的異常處理代碼。
當一個異常發生後,必須先檢查、處理、清除異常後再作其它JNI函數調用,不然的話,結果未知。當前線程中有異常的時候,你能夠調用的JNI函數很是少,11.8.2節列出了這些JNI函數的詳細列表。一般來講,當有一個未處理的異常時,你只能夠調用兩種JNI函數:異常處理函數和清除VM資源的函數。
當異常發生時,釋放資源是一件很重要的事,下面的例子中,調用GetStringChars函數後,若是後面的代碼發生異常,不要忘了調用ReleaseStringChars釋放資源。
<span style="font-family:Comic Sans MS;font-size:14px;">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);
 }
 </span>

2.3 工具函數中的異常

 
程序員編寫工具函數時,必定要把工具函數內部分發生的異常傳播到調用它的方法中去。這裏有兩個須要注意的地方:
一、對調用者來講,工具函數提供一個錯誤返回碼比簡單地把異常傳播過去更方便一些。
二、工具函數在發生異常時尤爲須要注意管理局部引用的方式。
爲了說明這兩點,咱們寫了一個工具函數,這個工具函數根據對象實例方法的名字和描述符作一些方法回調。
        
<span style="font-family:Comic Sans MS;font-size:14px;"> jvalue
          JNU_CallMethodByName(JNIEnv*env,
                               jboolean *hasException,
                               jobject obj,
                               const char *name,
                               const char *descriptor,...)
          {
              va_list args;
              jclass clazz;
              jmethodID mid;
              jvalue result;
              if ((*env)->EnsureLocalCapacity(env, 2)== JNI_OK) {
                  clazz = (*env)->GetObjectClass(env,obj);
                  mid = (*env)->GetMethodID(env,clazz, name,
                                            descriptor);
                  if (mid) {
                      const char *p = descriptor;
                      /* skip over argument types to findout the
                         return type */
                      while (*p != ')') p++;
                      /* skip ')' */
                      p++;
                      va_start(args, descriptor);
                      switch (*p) {
                      case 'V':
                          (*env)->CallVoidMethodV(env,obj, mid, args);
                          break;
                      case '[':
                      case 'L':
                          result.l =(*env)->CallObjectMethodV(
                                                 env,obj, mid, args);
                          break;
                      case 'Z':
                          result.z =(*env)->CallBooleanMethodV(
                                                 env,obj, mid, args);
                          break;
                      case 'B':
                          result.b =(*env)->CallByteMethodV(
                                                 env, obj, mid, args);
                          break;
                      case 'C':
                          result.c =(*env)->CallCharMethodV(
                                                 env,obj, mid, args);
                          break;
                      case 'S':
                          result.s =(*env)->CallShortMethodV(
                                                 env,obj, mid, args);
                          break;
                      case 'I':
                          result.i =(*env)->CallIntMethodV(
                                                 env,obj, mid, args);
                          break;
                      case 'J':
                          result.j =(*env)->CallLongMethodV(
                                                 env,obj, mid, args);
                          break;
                      case 'F':
                          result.f =(*env)->CallFloatMethodV(
                                                 env,obj, mid, args);
                          break;
                      case 'D':
                          result.d =(*env)->CallDoubleMethodV(
                                                 env,obj, mid, args);
                          break;
                      default:
                          (*env)->FatalError(env,"illegal descriptor");
                      }
                      va_end(args);
                  }
                  (*env)->DeleteLocalRef(env, clazz);
              }
              if (hasException) {
                  *hasException =(*env)->ExceptionCheck(env);
              }
              return result;
          }</span>


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) {
                  jthrowable exc =(*env)->ExceptionOccurred(env);
                  *hasException = exc != NULL;
                  (*env)->DeleteLocalRef(env, exc);
   }
爲了刪除指向異常對象的局部引用,DeleteLocalRef方法必須被調用。
使用JNU_CallMethodByName這個工具函數,咱們能夠重寫Instance-MethodCall.nativeMethod方法的實現:
  
<span style="font-family:Comic Sans MS;font-size:14px;">       JNIEXPORT void JNICALL
          Java_InstanceMethodCall_nativeMethod(JNIEnv*env, jobject obj)
          {
              printf("In C\n");
              JNU_CallMethodByName(env, NULL, obj,"callback", "()V");
          }</span>

調用JNU_CallMethodByName函數後,咱們不須要檢查異常,由於本地方法後面會當即返回。


測試代碼:html

	/**
	 * 異常處理
	 */
	public native void doExcepton() throws IllegalArgumentException;
	/**
	 * 
	 * @throws NullPointerException
	 */
	public void excepton() throws NullPointerException {
		throw new NullPointerException("doExcepton.excepton");
	}

jni:

/**
 * 異常處理
 */
JNIEXPORT void JNICALL Java_com_example_jniandroid_service_CFunction_doExcepton(
	JNIEnv * env, jobject obj) {
	jthrowable exc;
	jclass cls = (*env)->GetObjectClass(env, obj);
	jmethodID mid =
	(*env)->GetMethodID(env, cls, "excepton", "()V");
	if (mid == NULL) {
		LOGI(" MID IS NULL");
		return;
	}
	(*env)->CallVoidMethod(env, obj, mid);
	exc = (*env)->ExceptionOccurred(env);
	//有異常
	if (exc) {
		jclass newExcCls;
		(*env)->ExceptionDescribe(env);
		(*env)->ExceptionClear(env);
		newExcCls = (*env)->FindClass(env,"java/lang/IllegalArgumentException");
		if (newExcCls == NULL) {
			return;
		}
		(*env)->ThrowNew(env, newExcCls, "thrown from C code doExcepton");
	}
}


前面全部代碼下載java

相關文章
相關標籤/搜索