從第三章中可以看出JNI中的基本型別和Java中的基本型別都是一一對應的,接下來先看一下JNI的基本型別定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
typedef unsigned char jboolean; typedef unsigned short jchar; typedef short jshort; typedef float jfloat; typedef double jdouble; typedef int jint; #ifdef _LP64 /* 64-bit Solaris */ typedef long jlong; #else typedef long long jlong; #endif typedef signed char jbyte; |
基本型別很容易理解,就是對C/C++中的基本型別用typedef重新定義了一個新的名字,在JNI中可以直接訪問。
JNI把Java中的所有物件當作一個C指標傳遞到本地方法中,這個指標指向JVM中的內部資料結構,而內部的資料結構在記憶體中的儲存方式是不可見的。只能從JNIEnv指標指向的函式表中選擇合適的JNI函式來操作JVM中的資料結構。第三章的示例中,訪問java.lang.String對應的JNI型別jstring時,沒有像訪問基本資料型別一樣直接使用,因為它在Java是一個引用型別,所以在原生程式碼中只能通過GetStringUTFChars這樣的JNI函式來訪問字串的內容。
下面先看一個例子:
Sample.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.study.jnilearn; public class Sample { public native static String sayHello(String text); public static void main(String[] args) { String text = sayHello("yangxin"); System.out.println("Java str: " + text); } static { System.loadLibrary("Sample"); } } |
com_study_jnilearn_Sample.h和Sample.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_Sample */ #ifndef _Included_com_study_jnilearn_Sample #define _Included_com_study_jnilearn_Sample #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_Sample * Method: sayHello * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello (JNIEnv *, jclass, jstring); #ifdef __cplusplus } #endif #endif // Sample.c #include "com_study_jnilearn_Sample.h" /* * Class: com_study_jnilearn_Sample * Method: sayHello * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello (JNIEnv *env, jclass cls, jstring j_str) { const char *c_str = NULL; char buff[128] = {0}; jboolean isCopy; // 返回JNI_TRUE表示原字串的拷貝,返回JNI_FALSE表示返回原字串的指標 c_str = (*env)->GetStringUTFChars(env, j_str, &isCopy); printf("isCopy:%d\n",isCopy); if(c_str == NULL) { return NULL; } printf("C_str: %s \n", c_str); sprintf(buff, "hello %s", c_str); (*env)->ReleaseStringUTFChars(env, j_str, c_str); return (*env)->NewStringUTF(env,buff); } |
執行結果如下:
示例解析:
1> 訪問字串
sayHello函式接收一個jstring型別的引數text,但jstring型別是指向JVM內部的一個字串,和C風格的字串型別char*不同,所以在JNI中不能通把jstring當作普通C字串一樣來使用,必須使用合適的JNI函式來訪問JVM內部的字串資料結構。
GetStringUTFChars(env, j_str, &isCopy) 引數說明:
env:JNIEnv函式表指標
j_str:jstring型別(Java傳遞給原生程式碼的字串指標)
isCopy:取值JNI_TRUE和JNI_FALSE,如果值為JNI_TRUE,表示返回JVM內部源字串的一份拷貝,併為新產生的字串分配記憶體空間。如果值為JNI_FALSE,表示返回JVM內部源字串的指標,意味著可以通過指標修改源字串的內容,不推薦這麼做,因為這樣做就打破了Java字串不能修改的規定。但我們在開發當中,並不關心這個值是多少,通常情況下這個引數填NULL即可。
因為Java預設使用Unicode編碼,而C/C++預設使用UTF編碼,所以在原生程式碼中操作字串的時候,必須使用合適的JNI函式把jstring轉換成C風格的字串。JNI支援字串在Unicode和UTF-8兩種編碼之間轉換,GetStringUTFChars可以把一個jstring指標(指向JVM內部的Unicode字元序列)轉換成一個UTF-8格式的C字串。在上例中sayHello函式中我們通過GetStringUTFChars正確取得了JVM內部的字串內容。
2> 異常檢查
呼叫完GetStringUTFChars之後不要忘記安全檢查,因為JVM需要為新誕生的字串分配記憶體空間,當記憶體空間不夠分配的時候,會導致呼叫失敗,失敗後GetStringUTFChars會返回NULL,並丟擲一個OutOfMemoryError異常。JNI的異常和Java中的異常處理流程是不一樣的,Java遇到異常如果沒有捕獲,程式會立即停止執行。而JNI遇到未決的異常不會改變程式的執行流程,也就是程式會繼續往下走,這樣後面針對這個字串的所有操作都是非常危險的,因此,我們需要用return語句跳過後面的程式碼,並立即結束當前方法。
3> 釋放字串
在呼叫GetStringUTFChars函式從JVM內部獲取一個字串之後,JVM內部會分配一塊新的記憶體,用於儲存源字串的拷貝,以便原生程式碼訪問和修改。即然有記憶體分配,用完之後馬上釋放是一個程式設計的好習慣。通過呼叫ReleaseStringUTFChars函式通知JVM這塊記憶體已經不使用了,你可以清除了。注意:這兩個函式是配對使用的,用了GetXXX就必須呼叫ReleaseXXX,而且這兩個函式的命名也有規律,除了前面的Get和Release之外,後面的都一樣。
4> 建立字串
通過呼叫NewStringUTF函式,會構建一個新的java.lang.String字串物件。這個新建立的字串會自動轉換成Java支援的Unicode編碼。如果JVM不能為構造java.lang.String分配足夠的記憶體,NewStringUTF會丟擲一個OutOfMemoryError異常,並返回NULL。在這個例子中我們不必檢查它的返回值,如果NewStringUTF建立java.lang.String失敗,OutOfMemoryError這個異常會被在Sample.main方法中丟擲。如果NewStringUTF建立java.lang.String成功,則返回一個JNI引用,這個引用指向新建立的java.lang.String物件。
其它字串處理函式:
1> GetStringChars和ReleaseStringChars:這對函式和Get/ReleaseStringUTFChars函式功能差不多,用於獲取和釋放以Unicode格式編碼的字串。後者是用於獲取和釋放UTF-8編碼的字串。
2> GetStringLength:由於UTF-8編碼的字串以”結尾,而Unicode字串不是。如果想獲取一個指向Unicode編碼的jstring字串長度,在JNI中可通過這個函式獲取。
3> GetStringUTFLength:獲取UTF-8編碼字串的長度,也可以通過標準C函式strlen獲取
4> GetStringCritical和ReleaseStringCritical:提高JVM返回源字串直接指標的可能性
Get/ReleaseStringChars和Get/ReleaseStringUTFChars這對函式返回的源字串會後分配記憶體,如果有一個字串內容相當大,有1M左右,而且只需要讀取裡面的內容列印出來,用這兩對函式就有些不太合適了。此時用Get/ReleaseStringCritical可直接返回源字串的指標應該是一個比較合適的方式。不過這對函式有一個很大的限制,在這兩個函式之間的原生程式碼不能呼叫任何會讓執行緒阻塞或等待JVM中其它執行緒的本地函式或JNI函式。因為通過GetStringCritical得到的是一個指向JVM內部字串的直接指標,獲取這個直接指標後會導致暫停GC執行緒,當GC被暫停後,如果其它執行緒觸發GC繼續執行的話,都會導致阻塞呼叫者。所以在Get/ReleaseStringCritical這對函式中間的任何原生程式碼都不可以執行導致阻塞的呼叫或為新物件在JVM中分配記憶體,否則,JVM有可能死鎖。另外一定要記住檢查是否因為記憶體溢位而導致它的返回值為NULL,因為JVM在執行GetStringCritical這個函式時,仍有發生資料複製的可能性,尤其是當JVM內部儲存的陣列不連續時,為了返回一個指向連續記憶體空間的指標,JVM必須複製所有資料。下面程式碼演示這對函式的正確用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello (JNIEnv *env, jclass cls, jstring j_str) { const jchar* c_str= NULL; char buff[128] = "hello "; char* pBuff = buff + 6; /* * 在GetStringCritical/RealeaseStringCritical之間是一個關鍵區。 * 在這關鍵區之中,絕對不能呼叫JNI的其他函式和會造成當前執行緒中斷或是會讓當前執行緒等待的任何原生程式碼, * 否則將造成關鍵區程式碼執行區間垃圾回收器停止運作,任何觸發垃圾回收器的執行緒也會暫停。 * 其他觸發垃圾回收器的執行緒不能前進直到當前執行緒結束而啟用垃圾回收器。 */ c_str = (*env)->GetStringCritical(env,j_str,NULL); // 返回源字串指標的可能性 if (c_str == NULL) // 驗證是否因為字串拷貝記憶體溢位而返回NULL { return NULL; } while(*c_str) { *pBuff++ = *c_str++; } (*env)->ReleaseStringCritical(env,j_str,c_str); return (*env)->NewStringUTF(env,buff); } |
JNI中沒有Get/ReleaseStringUTFCritical這樣的函式,因為在進行編碼轉換時很可能會促使JVM對資料進行復制,因為JVM內部表示的字串是使用Unicode編碼的。
5> GetStringRegion和GetStringUTFRegion:分別表示獲取Unicode和UTF-8編碼字串指定範圍內的內容。這對函式會把源字串複製到一個預先分配的緩衝區內。下面程式碼用GetStringUTFRegion重新實現sayHello函式:
1 2 3 4 5 6 7 8 9 10 11 |
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello (JNIEnv *env, jclass cls, jstring j_str) { jsize len = (*env)->GetStringLength(env,j_str); // 獲取unicode字串的長度 printf("str_len:%d\n",len); char buff[128] = "hello "; char* pBuff = buff + 6; // 將JVM中的字串以utf-8編碼拷入C緩衝區,該函式內部不會分配記憶體空間 (*env)->GetStringUTFRegion(env,j_str,0,len,pBuff); return (*env)->NewStringUTF(env,buff); } |
GetStringUTFRegion這個函式會做越界檢查,如果檢查發現越界了,會丟擲StringIndexOutOfBoundsException異常,這個方法與GetStringUTFChars比較相似,不同的是,GetStringUTFRegion內部不分配記憶體,不會丟擲記憶體溢位異常。
注意:GetStringUTFRegion和GetStringRegion這兩個函式由於內部沒有分配記憶體,所以JNI沒有提供ReleaseStringUTFRegion和ReleaseStringRegion這樣的函式。
字串操作總結:
1、對於小字串來說,GetStringRegion和GetStringUTFRegion這兩對函式是最佳選擇,因為緩衝區可以被編譯器提前分配,而且永遠不會產生記憶體溢位的異常。當你需要處理一個字串的一部分時,使用這對函式也是不錯。因為它們提供了一個開始索引和子字串的長度值。另外,複製少量字串的消耗 也是非常小的。
2、使用GetStringCritical和ReleaseStringCritical這對函式時,必須非常小心。一定要確保在持有一個由 GetStringCritical 獲取到的指標時,原生程式碼不會在 JVM 內部分配新物件,或者做任何其它可能導致系統死鎖的阻塞性呼叫
3、獲取Unicode字串和長度,使用GetStringChars和GetStringLength函式
4、獲取UTF-8字串的長度,使用GetStringUTFLength函式
5、建立Unicode字串,使用NewStringUTF函式
6、從Java字串轉換成C/C++字串,使用GetStringUTFChars函式
7、通過GetStringUTFChars、GetStringChars、GetStringCritical獲取字串,這些函式內部會分配記憶體,必須呼叫相對應的ReleaseXXXX函式釋放記憶體
示例程式碼下載地址:https://code.csdn.net/xyang81/jnilearn