用 JNI 進行 Java 編程(4)

高級主題java

概述程序員

從 Java 程序內調用本機代碼破壞了 Java 程序的可移植性和安全性。儘管已編譯的 Java 字節碼保持了很好的可移植性,但必須爲您打算用來運行該應用程序的每一個平臺從新編譯本機代碼。另外,因爲本機代碼在 JVM 以外執行,因此約束它的安全性協議沒必要和 Java 代碼的相同。編程

從本機程序調用 Java 代碼也很複雜。由於 Java 語言是面向對象的,因此從本機應用程序調用 Java 代碼一般涉及面向對象技術。有些本機語言不支持面向對象編程或只是有限地支持面向對象編程(譬如 C),使用這些語言調用 Java 方法可能會產生問題。在本節中,咱們將討論使用 JNI 所帶來的若干複雜性,並研究解決它們的方法。數組

Java 字符串 vs. C 字符串

Java 字符串是做爲 16 位 Unicode 字符存儲的,而 C 字符串是做爲一組 8 位且以空字符爲結束的字符存儲的。JNI 提供了幾個有用的函數,它們用於在 Java 字符串和 C 字符串之間進行轉換並操做這兩種字符串。下面的代碼片斷演示瞭如何將 C 字符串轉換成 Java 字符串:安全


/* Convert a C string to a Java String. */
char[]  str  = "To be or not to be.\n";
jstring jstr = (*env)->NewStringUTF(env, str);

接下來,咱們研究將 Java 字符串轉換成 C 字符串的代碼。請注意第 5 行對 ReleaseStringUTFChars() 函數的調用。當您再也不使用 Java 字符串時,應該使用這個函數來釋放它們。當本機代碼再也不須要引用字符串時,請老是確保釋放它們。不這樣作可能致使內存泄漏。多線程


/* Convert a Java String into a C string. */
char buf[128; 
const char *newString = (*env)->GetStringUTFChars(env, jstr, 0);
...
(*env)->ReleaseStringUTFChars(env, jstr, newString);

Java 數組 vs. C 數組

與字符串相似,Java 數組和 C 數組在內存中的表示不一樣。幸運的是,一組 JNI 函數能夠提供指向數組中元素的指針。下圖顯示瞭如何將 Java 數組映射到 JNI C 類型。ide

C 類型 jarray 表示通用數組。在 C 語言中,全部數組類型實際上只是 jobject 的同義類型。可是,在 C++ 語言中,全部的數組類型都繼承了 jarrayjarray 又依次繼承了 jobject 。有關全部 C 類型對象的繼承圖,請參閱附錄 A:JNI 類型。函數

使用數組

一般,處理數組時,首先想到要作的是肯定其大小。爲了作到這一點,應該使用 GetArrayLength() 函數,它返回一個表示數組大小的 jsizeoop

接下來,會想要獲取一個指向數組元素的指針。可使用 GetXXXArrayElement()SetXXXArrayElement() 函數(根據數組的類型替換方法名中的 XXXObjectBooleanByteCharIntLong 等等)來訪問數組中的元素。編碼

當本機代碼完成了對 Java 數組的使用時,必須調用函數 ReleaseXXXArrayElements() 來釋放它。不然,可能致使內存泄漏。下面的代碼段顯示瞭如何循環遍歷一個整型數組的全部元素:


/* Looping through the elements in an array. */
int* elem = (*env)->GetIntArrayElements(env, intArray, 0);
for (i=0; I < (*env)->GetIntArrayLength(env, intArray); i++)
   sum += elem[i]
(*env)->ReleaseIntArrayElements(env, intArray, elem, 0);

局部引用 vs. 全局引用

當使用 JNI 編程時,會須要使用對 Java 對象的引用。缺省狀況下,JNI 建立局部引用以確保它們能夠被垃圾收集。因爲這一點,您可能會由於嘗試存儲一個本地引用,以便稍後重用它而無心間編寫出非法代碼,以下面的代碼樣本所示:


/* This code is invalid! */
static jmethodID mid;
 
JNIEXPORT jstring JNICALL
Java_Sample1_accessMethod(JNIEnv *env, jobject obj)
{
    ...
    cls = (*env)->GetObjectClass(env, obj);
    if (cls != 0)
    mid = (*env)->GetStaticMethodID(env, cls, "addInt", "(I)I");
    ...
}

由於第 10 行的錯誤,因此這個代碼段是非法的。midmethodID,而且 GetStaticMethodID() 返回 methodID。可是,返回的 methodID 是一個局部引用,而您不該該將一個局部引用賦給全局引用。而 mid 是一個全局引用。

Java_Sample1_accessMethod() 返回以後,mid 引用就再也不有效,由於賦給它如今超出做用域之外的局部引用。嘗試使用 mid 將致使錯誤結果或 JVM 崩潰。

建立全局引用

要糾正這個問題,須要建立和使用全局引用。全局引用將在顯式釋放以前一直有效,您必須記住去顯式地釋放它。沒有釋放引用可能致使內存泄漏。

使用 NewGlobalRef() 建立全局引用,並用 DeleteGlobalRef() 刪除它,以下面的代碼樣本所示

/* This code is valid! */
static jmethodID mid;
  
JNIEXPORT jstring JNICALL
 Java_Sample1_accessMethod(JNIEnv *env, jobject obj)
{
   ...
   cls = (*env)->GetObjectClass(env, obj);
   if (cls != 0)
   {
      mid1 = (*env)->GetStaticMethodID(env, cls, "addInt", "(I)I");
      mid = (*env)->NewGlobalRef(env, mid1);
      ...
}

錯誤處理

在 Java 程序中使用本機方法,就以某種基本的方式破壞了 Java 安全性模型。由於 Java 程序在一個受控的運行時系統(JVM)中運行,因此 Java 平臺設計師決定經過檢查常見運行時系統錯誤(如數組下標、越界錯誤、空指針錯誤)來幫助程序員。從另外一方面講,因爲 C 和 C++ 不使用此類錯誤檢查,因此本機方法程序員必須本身處理全部錯誤狀況,而在運行時,這些錯誤能夠在 JVM 中被捕獲。

例如,對於 Java 程序而言,經過拋出一個異常來向 JVM 報告出錯是常見和正確的操做。C 沒有異常,所以必須使用 JNI 的異常處理函數。

JNI 的異常處理函數

有兩種方法用來在本機代碼中拋出異常:能夠調用 Throw() 函數或 ThrowNew() 函數。在調用 Throw() 以前,首先須要建立一個 Throwable 類型的對象。能夠經過調用 ThrowNew() 跳過這一步,由於這個函數爲您建立了該對象。在下面的示例代碼片斷中,咱們使用這兩個函數拋出 IOException


/* Create the Throwable object. */
jclass cls = (*env)->FindClass(env, "java/io/IOException");
jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
jthrowable e = (*env)->NewObject(env, cls, mid);
 
/* Now throw the exception */
(*env)->Throw(env, e);
 ...
 
/* Here we do it all in one step and provide a message*/
 (*env)->ThrowNew(env,(*env)->FindClass("java/io/IOException"),
                  "An IOException occurred!");

Throw()ThrowNew() 函數並不中斷本機方法中的控制流。直到本機方法返回,在 JVM 中才會將異常實際拋出。在 C 中,一旦碰到錯誤條件,不能使用 Throw()ThrowNew() 函數當即退出方法,而在 Java 中,這可使用 throw 語句來退出方法。相反,須要在 Throw()ThrowNew() 函數以後當即使用 return 語句,以便在出錯點退出本機方法。

JNI 的異常捕獲函數

當從 C 或 C++ 調用 Java 時,也可能須要捕獲異常。 許多 JNI 函數都能拋出但願捕獲的異常。ExceptionCheck() 函數返回 jboolean 以代表是否拋出了異常,而 ExceptionOccured() 方法返回指向當前異常的 jthrowable 引用(或者返回 NULL,若是未拋出異常的話)。

若是正在捕獲異常,可能要處理異常,在這種狀況下須要在 JVM 中清除該異常。可使用 ExceptionClear() 函數來進行這個操做。ExceptionDescribed() 函數用來顯示異常的調試消息。

本機方法中的多線程

在使用 JNI 工做時,您將遇到的更高級的問題之一是在本機方法中使用多線程。即便是在不須要支持多線程的系統上運行時,Java 平臺也是做爲多線程系統來實現的;所以您有責任確保本機函數是線程安全的。

在 Java 程序中,能夠經過使用 synchronized 語句實現線程安全的代碼。synchronized 語句的語法使您可以獲取對象上的鎖。 只要在 synchronized 塊中,就能夠執行任何數據操做,而沒必要擔憂其它線程會悄悄進入並訪問您鎖定的對象。

JNI 使用 MonitorEnter()MonitorExit() 函數提供相似的結構。對於傳遞到 MonitorEnter() 函數中的對象,您會獲得一個用於該對象的監視器(鎖),並在使用 MonitorExit() 函數釋放它以前一直持有該鎖。對於您鎖定的對象而言,MonitorEnter()MonitorExit() 函數之間的全部代碼保證是線程安全的。

本機方法中的同步

下表顯示瞭如何在 Java、C 和 C++ 中同步一塊代碼。正如您所見,這些 C 和 C++ 函數相似於 Java 代碼中的 synchronized 語句。


XML error: The image is not displayed because the width is greater than the maximum of 580 pixels. Please decrease the image width.


隨本機方法一塊兒使用 synchronized

確保本機方法同步的另外一種方法是:當在 Java 類中聲明 native 方法時使用 synchronized 關鍵字。

使用 synchronized 關鍵字將確保任什麼時候候從 Java 程序調用 native 方法,它都將是 synchronized。 儘管用 synchronized 關鍵字來標記線程安全的本機方法是個好想法,但一般最好老是在本機方法實現中實現同步。這樣作的主要緣由以下:

  • C 或 C++ 代碼和 Java 本機方法聲明不一樣,所以,若是方法聲明有變更(即,若是一旦除去了 synchronized 關鍵字),此方法可能立刻再也不是線程安全的了。

  • 若是有人對使用該函數的其它本機方法(或其它 C 或 C++ 函數)進行編碼,他們可能並無意識到該本機實現不是線程安全的。

  • 若是將函數做爲普通的 C 函數在 Java 程序以外使用,則它不是線程安全的。

其它同步技術

Object.wait()Object.notify()Object.notifyAll() 方法也支持線程同步。由於全部 Java 對象都將 Object 類做爲父類,因此全部 Java 對象都有這些方法。您能夠象調用其它方法同樣,從本機代碼調用這些方法,並以 Java 代碼中相同的方式來使用它們,以實現線程同步。

相關文章
相關標籤/搜索