本章總結(jié)了JNI實(shí)際應(yīng)用中容易出錯的一些情況供JNI程序員參考。
10.1 錯誤檢查
編寫本地方法時最常見的錯誤就是忘記檢查是否發(fā)生了異常。我承認(rèn),JNI里面的異常檢查確實(shí)比較麻煩,但是,這很重要。
10.2 向JNI函數(shù)傳遞非法參數(shù)
JNI不會檢查參數(shù)是否正確,如果你自己不保證參數(shù)的正確有效,那么出現(xiàn)什么樣的錯誤是未知的。通常,不檢查參數(shù)的有效性在C/C++庫中是比較常見的。
10.3 把jclass和jobject弄混
一開始使用JNI時,很容易把對象引用(jobject類型的值)和類引用(jclass類型的值)弄混。對象引用對應(yīng)的是數(shù)組或者java.lang.Object及其子類的對象實(shí)例,而類引用對應(yīng)的是java.lang.Class的實(shí)例。
像GetFieldID這樣需要傳入jclass作為參數(shù)的方法做的是一個類操作,因?yàn)樗菑囊粋€類中獲取字段的描述。而GetIntField這樣需要傳入jobject作為參數(shù)的方法做的是一個對象操作,因?yàn)樗鼜囊粋€對象實(shí)例中獲取字段的值。
10.4jboolean會面臨數(shù)據(jù)截取的問題
Jboolean是一個8-bit unsigned的C類型,可以存儲0255的值。其中,0對應(yīng)常量JNI_FALSE,而1255對應(yīng)常量JNI_TRUE。但是,32或者16位的值,如果最低的8位是0的話,就會引起問題。
假設(shè)你定義了一個函數(shù)print,需要傳入一個jboolean類型的condition作為參數(shù):
void print(jboolean condition)
{
/* C compilers generate code that truncates condition
to its lower 8 bits. /
if (condition) {
printf("true\n");
} else {
printf("false\n");
}
}
對上面這段代碼來說,下面這樣用就會出現(xiàn)問題:
int n = 256; / the value 0x100, whose lower 8 bits are all 0 /
print(n);
我們傳入了一個非0的值256(0X100),因?yàn)檫@個值的低8位(即,0)被截出來使用,上面的代碼會打印“false”。
根據(jù)經(jīng)驗(yàn),這里有一個常用的解決方案:
n = 256;
print (n ? JNI_TRUE : JNI_FALSE);
10.5 編程的時候,什么用JAVA,什么時候用C?
這里有一些經(jīng)驗(yàn)性的注意事項(xiàng):
1、 盡量讓JAVA和C之間的接口簡單化,C和JAVA間的調(diào)用過于復(fù)雜的話,會使得BUG調(diào)試、代碼維護(hù)和JVM對代碼進(jìn)行優(yōu)化都會變得很難。比如虛擬機(jī)很容易對一些JAVA方法進(jìn)行內(nèi)聯(lián),但對本地方法卻無能為力。
2、 盡量少寫本地代碼。因?yàn)楸镜卮a即不安全又是不可移植的,而且本地代碼中的錯誤檢查很麻煩。
3、 讓本地代碼盡量獨(dú)立。也就是說,實(shí)際使用的時候,盡量讓所有的本地方法都在同一個包甚至同一個類中。
JNI把JVM的許多功能開發(fā)給了本地代碼:類加載、對象創(chuàng)建、字段訪問、方法調(diào)用、線程同步等。雖然用JAVA來做這些事情的時候很容易,但有時候,用本地代碼來做很誘人。下面的代碼會告訴你,為什么用本地代碼進(jìn)行JAVA編程是愚蠢的。假設(shè)我們需要創(chuàng)建一個線程并啟動它,JAVA代碼這樣寫:
new JobThread().start();
而用本地代碼卻需要這樣:
/ Assume these variables are precomputed and cached:
Class_JobThread: the class "JobThread"MID_Thread_init: method ID of constructorMID_Thread_start: method ID of Thread.start()
/
aThreadObject =
(env)->NewObject(env, Class_JobThread, MID_Thread_init);
if (aThreadObject == NULL) {
... /* out of memory /
}
(env)->CallVoidMethod(env, aThreadObject, MID_Thread_start);
if ((env)->ExceptionOccurred(env)) {
... / thread did not start /
}
比較起來,本地代碼寫會使用編程變得復(fù)雜,代碼量大,錯誤處理多。通常,如果不得不用本地代碼來做這些事的話,在JAVA中提供一個輔助函數(shù),并在本地代碼中對這個輔助函數(shù)進(jìn)行回調(diào)。
10.6 混淆ID和引用
本地代碼中使用引用來訪問JAVA對象,使用ID來訪問方法和字段。
引用指向的是可以由本地代碼來管理的JVM中的資源。比如DeleteLocalRef這個本地函數(shù),允許本地代碼刪除一個局部引用。而字段和方法的ID由JVM來管理,只有它所屬的類被unload時,才會失效。本地代碼不能顯式在刪掉一個字段或者方法的ID。
本地代碼可以創(chuàng)建多個引用并讓它們指向相同的對象。比如,一個全局引用和一個局部引用可能指向相同的對象。而字段ID和方法ID是唯一的。比如類A定義了一個方法f,而類B從類A中繼承了方法f,那么下面的調(diào)用結(jié)果是相同的:
jmethodID MID_A_f = (env)->GetMethodID(env, A, "f", "()V");
jmethodID MID_B_f = (env)->GetMethodID(env, B, "f", "()V");
10.7 緩存字段ID和方法ID
這里有一個緩存ID的例子:
class C {
private int i;
native void f();
}
下面是本地方法的實(shí)現(xiàn),沒有使用緩存ID。
// No field IDs cached.
JNIEXPORT void JNICALL
Java_C_f(JNIEnv env, jobject this) {
jclass cls = (env)->GetObjectClass(env, this);
... / error checking /
jfieldID fid = (env)->GetFieldID(env, cls, "i", "I");
... /* error checking /
ival = (env)->GetIntField(env, this, fid);
... /* ival now has the value of this.i */
}
上面的這些代碼一般可以運(yùn)行正確,但是下面的情況下,就出錯了:
// Trouble in the absence of ID caching
class D extends C {
private int i;
D() {
f(); // inherited from C
}
}
類D繼承了類C,并且也有一個私有的字段i。
當(dāng)在D的構(gòu)造方法中調(diào)用f時,本地方法接收到的參數(shù)中,cls指向提類D的對象,fid指向的是D.i這個字段。在這個本地方法的末尾,ival里面是D.i的值,而不是C.i的值。這與你想象的是不一樣的。
上面這種問題的解決方案是:
// Version that caches IDs in static initializers
class C {
private int i;
native void f();
private static native void initIDs();
static {
initIDs(); // Call an initializing native method
}
}
本地方法這樣實(shí)現(xiàn):
static jfieldID FID_C_i;
JNIEXPORT void JNICALL
Java_C_initIDs(JNIEnv env, jclass cls) {
/ Get IDs to all fields/methods of C that
native methods will need. /
FID_C_i = (env)->GetFieldID(env, cls, "i", "I");
}
JNIEXPORT void JNICALL
Java_C_f(JNIEnv env, jobject this) {
ival = (env)->GetIntField(env, this, FID_C_i);
... /* ival is always C.i, not D.i */
}
字段ID在類C的靜態(tài)初始時被計(jì)算并緩存下來,這樣就可以確保緩存的是C.i的ID,因此,不管本地方法中接收到的jobject是哪個類的實(shí)例,訪問的永遠(yuǎn)是C.i的值。
另外,同樣的情況也可能會出現(xiàn)在方法ID上面。
10.8 Unicode字符串結(jié)尾
從GetStringChars和GetStringCritical兩個方法獲得的Unicode字符串不是以NULL結(jié)尾的,需要調(diào)用GetStringLength來獲取字符串的長度。一些操作系統(tǒng),如Windows NT中,Unicode字符串必須以兩個’\0’結(jié)尾,這樣的話,就不能直接把GetStringChars得到的字符串傳遞給Windows NT系統(tǒng)的API,而必須復(fù)制一份并在字符串的結(jié)尾加入兩個“\0”
10.9 訪問權(quán)限失效
在本地代碼中,訪問方法和變量時不受JAVA語言規(guī)定的限制。比如,可以修改private和final修飾的字段。并且,JNI中可以訪問和修改heap中任意位置的內(nèi)存。這些都會造成意想不到的結(jié)果。比如,本地代碼中不應(yīng)該修改java.lang.String和java.lang.Integer這樣的不可變對象的內(nèi)容。否則,會破壞JAVA規(guī)范。
10.10 忽視國際化
JVM中的字符串是Unicode字符序列,而本地字符串采用的是本地化的編碼。實(shí)際編碼的時候,我們經(jīng)常需要使用像JNU_NewStringNative和JNU_GetStringNativeChars這樣的工具函數(shù)來把Unicode編碼的jstring轉(zhuǎn)化成本地字符串,要對消息和文件名尤其關(guān)注,它們經(jīng)常是需要國際化的,可能包含各種字符。
如果一個本地方法得到了一個文件名,必須把它轉(zhuǎn)化成本地字符串之后才能傳遞給C庫函數(shù)使用:
JNIEXPORT jint JNICALL
Java_MyFile_open(JNIEnv *env, jobject self, jstring name,
jint mode)
{
jint result;
char *cname = JNU_GetStringNativeChars(env, name);
if (cname == NULL) {
return 0;
}
result = open(cname, mode);
free(cname);
return result;
}
上例中,我們使用JNU_GetStringNativeChars把Unicode字符串轉(zhuǎn)化成本地字符串。
10.11 確保釋放VM資源
JNI編程時常見的錯誤之一就是忘記釋放VM資源,尤其是在執(zhí)行路徑分支時,比如,有異常發(fā)生的時候:
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv env, jclass cls, jstring jstr)
{
const jchar cstr =
(env)->GetStringChars(env, jstr, NULL);
if (cstr == NULL) {
return;
}
...
if (...) { / exception occurred /
/ misses a ReleaseStringChars call /
return;
}
...
/ normal return /
(env)->ReleaseStringChars(env, jstr, cstr);
}
忘記調(diào)用ReleaseStringChars可能導(dǎo)致jstring永遠(yuǎn)被VM給pin著不被回收。一個GetStringChars必然要對應(yīng)著一個ReleaseStringChars,下面的代碼就沒有正確地釋放VM資源:
/* The isCopy argument is misused here! */
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv env, jclass cls, jstring jstr)
{
jboolean isCopy;
const jchar cstr = (env)->GetStringChars(env, jstr,
&isCopy);
if (cstr == NULL) {
return;
}
... / use cstr /
/ This is wrong. Always need to call ReleaseStringChars. /
if (isCopy) {
(env)->ReleaseStringChars(env, jstr, cstr);
}
}
即使在isCopy的值是JNI_FALSE時,也應(yīng)該調(diào)用ReleaseStringChars在unpin掉jstring。
10.12 過多的創(chuàng)建局部引用
大量的局部引用創(chuàng)建會浪費(fèi)不必要的內(nèi)存。一個局部引用會導(dǎo)致它本身和它所指向的對象都得不到回收。尤其要注意那些長時間運(yùn)行的方法、創(chuàng)建局部引用的循環(huán)和工具函數(shù),充分得利用Pus/PopLocalFrame來高效地管理局部引用。
10.13 使用已經(jīng)失效的局部引用
局部引用只在一個本地方法的調(diào)用期間有效,方法執(zhí)行完成后會被自動釋放。本地代碼不應(yīng)該把存儲局部引用存儲到全局變量中在其它地方使用。
10.14 跨進(jìn)程使用JNIEnv
JNIEnv這個指針只能在當(dāng)前線程中使用,不要在其它線程中使用。
10.15 錯誤的線程模型(Thread Models)
搞不明白,不翻了。。。