在 JNI 編程中常需要從一個(gè)普通的 C/C++ 函數(shù)中調(diào)用 JNI 方法,比如:
long ProcessKeyboardEventInJava(int keyboardEvent) { long error = 0; JNIEnv* env = g_env; jclass that = g_class; jmethodID mid = g_mid; … … error = env->CallStaticIntMethod(that, mid, keyboardEvent); return error; } |
ProcessKeyboardEventInJava 只是個(gè)普通的 C 函數(shù),目的是把參數(shù)中的鍵盤(pán)事件交給 Java 代碼處理。它并沒(méi)有 JNI 參數(shù),卻需要調(diào)用一個(gè) JNI 方法。那么調(diào)用所必需的參數(shù) JNIEnv,jclass 和 jmethodID,是如何獲得呢?從上面的代碼中可以看出,它們來(lái)自保存的全局變量 g_env,g_class 和 g_mid。這樣做對(duì)么?安全么?也許這段代碼在某種情況下,實(shí)現(xiàn)了軟件的需求,成功地執(zhí)行了 Java 的函數(shù),處理了這個(gè)鍵盤(pán)事件。在這種情況下,設(shè)計(jì)者很清楚,也深信 ProcessKeyboardEventInJava 只會(huì)在某個(gè) Native 函數(shù)返回前被調(diào)用。對(duì)應(yīng)的場(chǎng)景代碼應(yīng)該是這樣的:
JNIEnv * g_env; jclass g_class; jmethodID g_mid; JNIEXPORT jint JNICALL Java_SampleTest_init (JNIEnv *env, jclass cls) { g_class = cls; g_env = env; g_mid = env->GetStaticMethodID(cls, "ProcessKeyboardEventInJava", "(III)I"); while(true) { …. long error; error = ProcessKeyboardEventInJava(theEvent) …. } } |
但萬(wàn)一 ProcessKeyboardEventInJava 在 Java_SampleTest_init(以下簡(jiǎn)稱 init)返回后的其他時(shí)機(jī)被調(diào)用了,會(huì)怎樣呢?那時(shí)可就沒(méi)有這么幸運(yùn)了,初衷是肯定達(dá)不到了,而發(fā)生 crash 或者 segmentation fault 這類錯(cuò)誤則幾乎是一定的。事實(shí)上,如上設(shè)計(jì),ProcessKeyboardEventInJava 也只允許在 init 函數(shù)中被調(diào)用。在其返回后被調(diào)用,就不合法了。所以,如果你是這個(gè)設(shè)計(jì)師,之前的設(shè)計(jì)中,是不是這個(gè)假設(shè)也有些太樂(lè)觀了呢?
![]() ![]() |
![]()
|
調(diào)用者本來(lái)以為這個(gè)函數(shù)能幫他的大忙,現(xiàn)在卻疑惑了,到底發(fā)生了什么?做錯(cuò)了什么呢?
原因其實(shí)很簡(jiǎn)單,上面三個(gè)全局變量中的 g_class 已經(jīng)是非法的了,生命周期在退出 init 之后就結(jié)束了。
這里要澄清的是 g_mid,由于它不是一個(gè) jobject,所以只要它對(duì)應(yīng)的 class 沒(méi)有被卸載,在退出 init 之后仍可以使用,沒(méi)問(wèn)題;而 g_env 對(duì)于同一個(gè) thread 來(lái)說(shuō),它是唯一的,所以只要是 init 和 ProcessKeyboardEventInJava 處于一個(gè) thread,初始化后它的值在這個(gè) thread 沒(méi)有中止之前,都一直是合法的。
那如何才能解決 g_class 的非法引用帶來(lái)的問(wèn)題呢?
這首先涉及到 Java 和 Native 代碼之間函數(shù)調(diào)用時(shí),參數(shù)如何傳遞的問(wèn)題。簡(jiǎn)單類型,也就是內(nèi)置類型,比如 int, char 等是值傳遞(pass by value),而其它 Java 對(duì)象都是引用傳遞(pass by reference),這些對(duì)象引用由 JVM 傳給 Native 代碼,每個(gè)都有其生命周期。
其次,Java 對(duì)象的生命周期是由它的引用類型決定的,這里的引用分兩種:local reference 和 global reference。Native 函數(shù)參數(shù)中 jobject 或者它的子類,其參數(shù)都是 local reference。Local reference 只在這個(gè) Native 函數(shù)中有效,Native 函數(shù)返回后,引用的對(duì)象就被釋放,它的生命周期就結(jié)束了。若要留著日后使用,則需根據(jù)這個(gè) local reference 創(chuàng)建 global reference。Global reference 不會(huì)被系統(tǒng)自動(dòng)釋放,它僅當(dāng)被程序明確調(diào)用 DeleteGlobalReference 時(shí)才被回收。
![]() ![]() |
![]()
|
于是解決的辦法就出來(lái)了:
在 init 函數(shù)中,不是簡(jiǎn)單地把 jcalss 參數(shù)保存,而是:
JNIEXPORT jint JNICALL Java_SampleTest_init (JNIEnv *env, jclass cls) { …. g_class= (jclass)(env->NewGlobalRef(cls)); …. } |
這樣,無(wú)論 ProcessKeyboardEventInJava 是在 init 返回前還是返回后,調(diào)用它都是安全的,可行的了。
![]() ![]() |
![]()
|
若要在某個(gè) Native 代碼返回后,還希望能繼續(xù)使用 JVM 提供的參數(shù)(比如 init 函數(shù)中的 jclass), 或者是過(guò)程中調(diào)用 JNI 函數(shù)的返回值(比如 g_mid),只要它是一個(gè) jobject, 則需要為它創(chuàng)建一個(gè) global reference,以后只能使用這個(gè) global reference;若不是一個(gè) jobject,則無(wú)需這么做。
jclass 是由 jobject public 繼承而來(lái)的子類,所以它當(dāng)然是一個(gè) jobject,需要?jiǎng)?chuàng)建一個(gè) global reference 以便日后使用。而 jmethodID/jfieldID 與 jobject 沒(méi)有繼承關(guān)系,它不是一個(gè) jobject,只是個(gè)整數(shù),所以不存在被釋放與否的問(wèn)題,可保存后直接使用。JNIEnv 對(duì)于每個(gè) thread 而言是唯一的,不能也不需要對(duì)它調(diào)用 NewGlobalReference。
聯(lián)系客服