現(xiàn)在林束,你知道了如何通過JNI來訪問JVM中的基本類型數(shù)據(jù)和字符串惹恃、數(shù)組這樣的引用類型數(shù)據(jù),下一步就是學(xué)習(xí)怎么樣和JVM中任意對象的字段和方法進(jìn)行交互恃锉。比如從本地代碼中調(diào)用JAVA中的方法,也就是通常說的來自本地方法中的callbacks(回調(diào))呕臂。
我們從進(jìn)行字段訪問和方法回調(diào)時需要的JNI函數(shù)開始講解破托。本章的稍后部分我們會討論怎么樣通過一些cache(緩存)技術(shù)來優(yōu)化這些操作。在最后歧蒋,我們還會討論從本地代碼中訪問字段和回調(diào)方法時的效率問題土砂。
4.1 訪問字段
JAVA支持兩種field(字段),每一個對象的實例都有一個對象字段的復(fù)制谜洽;所有的對象共享一個類的靜態(tài)字段萝映。本地方法使用JNI提供的函數(shù)可以獲取和修改這兩種字段。先看一個從本地代碼中訪問對象字段的例子:
class InstanceFieldAccess {
private String s;
private native void accessField();
public static void main(String args[]) {
InstanceFieldAccess c = new InstanceFieldAccess();
c.s = "abc";
c.accessField();
System.out.println("In Java:");
System.out.println(" c.s = \"" + c.s + "\"");
}
static {
System.loadLibrary("InstanceFieldAccess");
}
}
InstanceFieldAccess這個類定義了一個對象字段s阐虚。main方法創(chuàng)建了一個對象并設(shè)置s的值序臂,然后調(diào)用本地方法InstanceFieldAccess.accessField在本地代碼中打印s的值,并把它修改為一個新值实束。本地方法返回后奥秆,JAVA中把這個值再打印一次,可以看出來咸灿,字段s的值已經(jīng)被改變了构订。下面是本地方法的實現(xiàn):
JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv env, jobject obj)
{
jfieldID fid; / store the field ID */
jstring jstr;
const char *str;
/* Get a reference to obj's class */
jclass cls = (*env)->GetObjectClass(env, obj);
printf("In C:\n");
/* Look for the instance field s in cls */
fid = (*env)->GetFieldID(env, cls, "s",
"Ljava/lang/String;");
if (fid == NULL) {
return; /* failed to find the field */
}
/* Read the instance field s */
jstr = (*env)->GetObjectField(env, obj, fid);
str = (*env)->GetStringUTFChars(env, jstr, NULL);
if (str == NULL) {
return; /* out of memory */
}
printf(" c.s = \"%s\"\n", str);
(*env)->ReleaseStringUTFChars(env, jstr, str);
/* Create a new string and overwrite the instance field */
jstr = (*env)->NewStringUTF(env, "123");
if (jstr == NULL) {
return; /* out of memory */
}
(*env)->SetObjectField(env, obj, fid, jstr);
}
運行程序,得到輸出為:
In C:
c.s = "abc"
In Java:
c.s = "123"
4.1.1 訪問一個對象字段的流程
為了訪問一個對象的實例字段避矢,本地方法需要做兩步:
首先悼瘾,通過在類引用上調(diào)用GetFieldID獲取field ID(字段ID)、字段名字和字段描述符:
Fid=(env)->GetFieldID(env,cls,”s”,”Ljava/lang/String;”);
上例中的代碼通過在對象引用obj上調(diào)用GetObjectClass獲取到類引用审胸。一旦獲取到字段ID亥宿,你就可以把對象和字段ID作為參數(shù)來訪問字段:
Jstr=(env)->GetObjectField(env,obj,fid);
因為字符串和數(shù)組是特殊的對象,所以我們使用GetObjectField來訪問字符串類型的實例字段歹嘹。除了Get/SetObjectField箩绍,JNI還支持其它如GetIntField、SetFloatField等用來訪問基本類型字段的函數(shù)尺上。
4.1.2 字段描述符
在上一節(jié)我們使用過一個特殊的C字符串“Ljava/lang/String”來代表一個JVM中的字段類型材蛛。這個字符串被稱為JNI field descriptor(字段描述符)圆到。
字符串的內(nèi)容由字段被聲明的類型決定。例如卑吭,使用“I”來表示一個int類型的字段芽淡,“F”來表示一個float類型的字段,“D”來表示一個double類型的字段豆赏,“Z”來表示一個boolean類型的字段等等挣菲。
像java.lang.String這樣的引用類型的描述符都是以L開頭,后面跟著一個JNI類描述符掷邦,以分號結(jié)尾白胀。一個JAVA類的全名中的包名分隔符“.”被轉(zhuǎn)化成“/”。因此抚岗,對于一個字段類型的字段來說或杠,它的描述符是“Ljava/lang/String”。
數(shù)組的描述符中包含“]”字符宣蔚,后面會跟著數(shù)組類型的描述符向抢,如“[I”是int[]類型的字段的描述符。12.3.3詳細(xì)介紹了各種類型的字段描述以及他們代表的JAVA類型胚委。
你可以使用javap工具來生成字段描述符挟鸠。
4.1.3 訪問靜態(tài)字段
訪問靜態(tài)字段和訪問實例字段相似,看下面這個InstanceFieldAccess例子的變形:
class StaticFielcdAccess {
private static int si;
private native void accessField();
public static void main(String args[]) {
StaticFieldAccess c = new StaticFieldAccess();
StaticFieldAccess.si = 100;
c.accessField();
System.out.println("In Java:");
System.out.println(" StaticFieldAccess.si = " + si);
}
static {
System.loadLibrary("StaticFieldAccess");
}
}
StaticFieldAccess這個類包含一個靜態(tài)字段si亩冬,main方法創(chuàng)建了一個對象艘希,初始化靜態(tài)字段,然后調(diào)用本地方法StaticFieldAccess.accessField在本地代碼中打印靜態(tài)字段中的值鉴未,然后設(shè)置新的值枢冤,為了演示這個值確實被改變了,在本地方法返回后铜秆,JAVA中再次這個靜態(tài)字段的值淹真。
下面是本地方法StaticFieldAccess.accessField的實現(xiàn):
JNIEXPORT void JNICALL
Java_StaticFieldAccess_accessField(JNIEnv env, jobject obj)
{
jfieldID fid; / store the field ID */
jint si;
/* Get a reference to obj's class */
jclass cls = (*env)->GetObjectClass(env, obj);
printf("In C:\n");
/* Look for the static field si in cls */
fid = (*env)->GetStaticFieldID(env, cls, "si", "I");
if (fid == NULL) {
return; /* field not found */
}
/* Access the static field si */
si = (*env)->GetStaticIntField(env, cls, fid);
printf(" StaticFieldAccess.si = %d\n", si);
(*env)->SetStaticIntField(env, cls, fid, 200);
}
運行程序可得到輸出結(jié)果:
In C:
StaticFieldAccess.si = 100
In Java:
StaticFieldAccess.si = 200
訪問靜態(tài)字段和對象實例字段的不同點:
1、 訪問靜態(tài)字段使用GetStaticFieldID连茧,而訪問對象的實例字段使用GetFieldID核蘸,但是,這兩個方法都有相同的返回值類型:jfieldID啸驯。
4.2 調(diào)用方法
JAVA中有幾種不同類型的方法客扎,實例方法必須在一個類的某個對象實例上面調(diào)用。而靜態(tài)方法可以在任何一個對象實例上調(diào)用罚斗。對于構(gòu)建方法的調(diào)用我們推遲到下一節(jié)徙鱼。
JNI支持一系列完整的函數(shù)讓你可以在本地代碼中回調(diào)JAVA方法,下面例子演示了如何從本地代碼中調(diào)用一個JAVA中的實例方法:
class InstanceMethodCall {
private native void nativeMethod();
private void callback() {
System.out.println("In Java");
}
public static void main(String args[]) {
InstanceMethodCall c = new InstanceMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary("InstanceMethodCall");
}
}
下面的是本地方法的實現(xiàn):
JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv env, jobject obj)
{
jclass cls = (env)->GetObjectClass(env, obj);
jmethodID mid =
(env)->GetMethodID(env, cls, "callback", "()V");
if (mid == NULL) {
return; / method not found /
}
printf("In C\n");
(env)->CallVoidMethod(env, obj, mid);
}
運行程序,得到如下輸出:
In C
In Java
4.2.1 調(diào)用實例方法
本地方法Java_InstanceMethodCall_nativeMethod的實現(xiàn)演示了在本地代碼中調(diào)用JAVA方法的兩步:
1袱吆、 本地方法首先調(diào)用JNI函數(shù)GetMethodID厌衙。這個函數(shù)在指定的類中尋找相應(yīng)的方法。這個尋找過程是基于方法描述符的绞绒。如果方法不存在婶希,GetMethodID返回NULL。這時蓬衡,立即從本地方法中返回喻杈,并引發(fā)一個NoSuchMethodError錯誤。
2狰晚、 本地方法通過調(diào)用CallVoidMethod來調(diào)用返回值為void的實例方法筒饰。
除了CallVoidMethod這個函數(shù)以外,JNI也支持對返回值為其它類型的方法的調(diào)用家肯。如果你調(diào)用的方法返回值類型為int龄砰,你的本地方法會使用CallIntMethod。類似地讨衣,你可以調(diào)用CallObjectMethod來調(diào)用返回值為java.lang.String、數(shù)組等對象類型的方法式镐。
你也可以使用Call<Type>Method系列的函數(shù)來調(diào)用接口方法反镇。你必須從接口類型中獲取方法ID,下面的代碼演示了如何在java.lang.Thread實例上面調(diào)用Runnable.run方法:
jobject thd = ...; /* a java.lang.Thread instance /
jmethodID mid;
jclass runnableIntf =
(env)->FindClass(env, "java/lang/Runnable");
if (runnableIntf == NULL) {
... /* error handling /
}
mid = (env)->GetMethodID(env, runnableIntf, "run", "()V");
if (mid == NULL) {
... /* error handling /
}
(env)->CallVoidMethod(env, thd, mid);
... /* check for possible exceptions */
在3.3.5中娘汞,我們使用FindClass來獲取一個類的引用歹茶,在這里,我們可以學(xué)到如何獲取一個接口的引用你弦。
4.2.2 生成方法描述符
JNI中描述字段使用字段描述符惊豺,描述方法同樣有方法描述符。一個方法描述符包含參數(shù)類型和返回值類型禽作。參數(shù)類型出現(xiàn)在前面尸昧,并由一對圓括號將它們括起來,參數(shù)類型按它們在方法聲明中出現(xiàn)的順序被列出來旷偿,并且多個參數(shù)類型之間沒有分隔符烹俗。如果一個方法沒有參數(shù),被表示為一對空圓括號萍程。方法的返回值類型緊跟參數(shù)類型的右括號后面幢妄。
例如,“(I)V”表示這個方法的一個參數(shù)類型為int茫负,并且有一個void類回值蕉鸳。“()D”表示這個方法沒有參數(shù)忍法,返回值類型為double潮尝。
方法描述符中可能會包含類描述符(12.3.2)榕吼,如方法native private String getLine(String);的描述符為:“(Ljava/lang/String;)Ljava/lang/String;”
數(shù)組類型的描述符以“[”開頭,后面跟著數(shù)組元素類型的描述符衍锚。如友题,public static void main(String[] args);的描述符是:"([Ljava/lang/String;)V"
12.3.4詳細(xì)描述了怎么樣生成一個JNI方法描述符。同樣戴质,你可以使用javap工具來打印出JNI方法描述符度宦。
4.2.3 調(diào)用靜態(tài)方法
前一個例子演示了一個本地方法怎樣調(diào)用實例方法。類似地告匠,本地方法中同樣可以調(diào)用靜態(tài)方法:
1戈抄、 通過GetStaticMethodID獲取方法ID。對應(yīng)于調(diào)用實例方法時的GetMethodID后专。
2划鸽、 傳入類、方法ID戚哎、參數(shù)裸诽,并調(diào)用提供靜態(tài)方法調(diào)用功能的JNI系列函數(shù)中的一個,如:CallStaticVoidMethod型凳,CallStaticBooleanMethod等丈冬。
調(diào)用靜態(tài)方法和調(diào)用實例方法的JNI函數(shù)有一個很大的不同,前者第二個參數(shù)是類引用甘畅,后者是對象實例的引用埂蕊。
在JAVA訪問一個靜態(tài)方法可以通過類,也可以通過對象實例疏唾。而JNI的規(guī)定是蓄氧,在本地代碼中回調(diào)JAVA中的靜態(tài)方法時,必須指定一個類引用才行槐脏。下面的例子演示了這個用法:
class StaticMethodCall {
private native void nativeMethod();
private static void callback() {
System.out.println("In Java");
}
public static void main(String args[]) {
StaticMethodCall c = new StaticMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary("StaticMethodCall");
}
}
下面是本地方法的實現(xiàn):
JNIEXPORT void JNICALL
Java_StaticMethodCall_nativeMethod(JNIEnv env, jobject obj)
{
jclass cls = (env)->GetObjectClass(env, obj);
jmethodID mid =
(env)->GetStaticMethodID(env, cls, "callback", "()V");
if (mid == NULL) {
return; / method not found /
}
printf("In C\n");
(env)->CallStaticVoidMethod(env, cls, mid);
}
當(dāng)調(diào)用CallStaticVoidMethod時喉童,確保你傳入的是類引用cls而不是對象引用obj。運行程序准给,輸出為:
In C
In Java
4.2.4 調(diào)用父類的實例方法
如果一個方法被定義在父類中泄朴,在子類中被覆蓋,你也可以調(diào)用這個實例方法露氮。JNI提供了一系列完成這些功能的函數(shù):CallNonvirtual<Type>Method祖灰。為了調(diào)用一個定義在父類中的實例方法,你必須遵守下面的步驟:
1畔规、 使用GetMethodID從一個指向父類的引用當(dāng)中獲取方法ID局扶。
2、 傳入對象、父類三妈、方法ID和參數(shù)畜埋,并調(diào)用CallNonvirtualVoidMethod、CallNonvirtualBooleanMethod等一系列函數(shù)中的一個畴蒲。
這種調(diào)用父類實例方法的情況其實很少遇到悠鞍,通常在JAVA中可以很簡單地做到:super.f();
CallNonvirtualVoidMethod也可以被用來調(diào)用父類的構(gòu)造函數(shù)。這個在下節(jié)就會講到模燥。
4.3 調(diào)用構(gòu)造函數(shù)
JNI中咖祭,構(gòu)造函數(shù)可以和實例方法一樣被調(diào)用隅津,調(diào)用方式也相似异赫。傳入“<init>”作為方法名蹭睡,“V”作為返回類型织阳。你可以通過向JNI函數(shù)NewObject傳入方法來調(diào)用構(gòu)造函數(shù)。下面的代碼實現(xiàn)了與JNI函數(shù)NewString相同的功能:把存儲在C緩沖區(qū)內(nèi)的Unicode編碼的字符序列田巴,創(chuàng)建成一個java.lang.String對象:
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
jclass stringClass;
jmethodID cid;
jcharArray elemArr;
jstring result;
stringClass = (*env)->FindClass(env, "java/lang/String");
if (stringClass == NULL) {
return NULL; /* exception thrown */
}
/* Get the method ID for the String(char[]) constructor /
cid = (env)->GetMethodID(env, stringClass,
"<init>", "([C)V");
if (cid == NULL) {
return NULL; /* exception thrown */
}
/* Create a char[] that holds the string characters */
elemArr = (*env)->NewCharArray(env, len);
if (elemArr == NULL) {
return NULL; /* exception thrown */
}
(*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
/* Construct a java.lang.String object */
result = (*env)->NewObject(env, stringClass, cid, elemArr);
/* Free local references */
(*env)->DeleteLocalRef(env, elemArr);
(*env)->DeleteLocalRef(env, stringClass);
return result;
}
上面這個本地方法有些復(fù)雜超歌,需要詳細(xì)解釋一下专普。首先补胚,F(xiàn)indClass返回一個java.lang.String類的引用码耐,接著,GetMethodID返回構(gòu)造函數(shù)String(char[] chars)的方法ID溶其。我們調(diào)用NewCharArray分配一個字符數(shù)組來保存字符串元素伐坏。JNI函數(shù)NewObject調(diào)用方法ID所標(biāo)識的構(gòu)造函數(shù)。NewObject函數(shù)需要的參數(shù)有:類的引用握联、構(gòu)造方法的ID、構(gòu)造方法需要的參數(shù)每瞒。
DeleteLocalRef允許VM釋放被局部引用elemArr和stringClass引用的資源金闽。5.2.1中詳細(xì)描述了調(diào)用DeleteLocalRef的時機(jī)和原因。
這個例子引出了一個問題剿骨,既然我們可以利用JNI函數(shù)來實現(xiàn)相同的功能代芜,為什么JNI還需要NewString這樣的內(nèi)置函數(shù)?原因是浓利,內(nèi)置函數(shù)的效率遠(yuǎn)高于在本地代碼里面調(diào)用構(gòu)造函數(shù)的API挤庇。而字符串又是最常用到的對象類型,因此需要在JNI中給予特殊的支持贷掖。
你也可以做到通過CallNonvirtualVoidMethod函數(shù)來調(diào)用構(gòu)造函數(shù)嫡秕。這種情況下,本地代碼必須首先通過調(diào)用AllocObject函數(shù)創(chuàng)建一個未初始化的對象苹威。上面例子中的result = (env)->NewObject(env, stringClass, cid, elemArr);可以被如下代碼替換:
result = (env)->AllocObject(env, stringClass);
if (result) {
(env)->CallNonvirtualVoidMethod(env, result, stringClass,
cid, elemArr);
/ we need to check for possible exceptions /
if ((env)->ExceptionCheck(env)) {
(*env)->DeleteLocalRef(env, result);
result = NULL;
}
}
AllocObject創(chuàng)建了一個未初始化的對象昆咽,使用時一定要非常小心,確保一個對象上面,構(gòu)造函數(shù)最多被調(diào)用一次掷酗。本地代碼不應(yīng)該在一個對象上面調(diào)用多次構(gòu)造函數(shù)调违。有時,你可能會發(fā)現(xiàn)創(chuàng)建一個未初始化的對象然后一段時間以后再調(diào)用構(gòu)造函數(shù)的方式是很有用的泻轰。盡管如此技肩,大部分情況下,你應(yīng)該使用NewObject浮声,盡量避免使用容易出錯的AllocObject/CallNonvirtualVoidMethod方法虚婿。
4.4 緩存字段ID和方法ID
獲取字段ID和方法ID時,需要用字段阿蝶、方法的名字和描述符進(jìn)行一個檢索雳锋。檢索過程相對比較費時,因此本節(jié)討論用緩存技術(shù)來減少這個過程帶來的消耗羡洁。緩存字段ID和方法ID的方法主要有兩種玷过。兩種區(qū)別主要在于緩存發(fā)生的時刻,是在字段ID和方法ID被使用的時候筑煮,還是定義字段和方法的類靜態(tài)初始化的時候辛蚊。
4.4.1 使用時緩存
字段ID和方法ID可以在字段的值被訪問或者方法被回調(diào)的時候緩存起來。下面的代碼中把字段ID存儲在靜態(tài)變量當(dāng)中真仲,這樣當(dāng)本地方法被重復(fù)調(diào)用時袋马,不必重新搜索字段ID:
JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv env, jobject obj)
{
static jfieldID fid_s = NULL; / cached field ID for s */
jclass cls = (*env)->GetObjectClass(env, obj);
jstring jstr;
const char *str;
if (fid_s == NULL) {
fid_s = (*env)->GetFieldID(env, cls, "s",
"Ljava/lang/String;");
if (fid_s == NULL) {
return; /* exception already thrown */
}
}
printf("In C:\n");
jstr = (*env)->GetObjectField(env, obj, fid_s);
str = (*env)->GetStringUTFChars(env, jstr, NULL);
if (str == NULL) {
return; /* out of memory */
}
printf(" c.s = \"%s\"\n", str);
(*env)->ReleaseStringUTFChars(env, jstr, str);
jstr = (*env)->NewStringUTF(env, "123");
if (jstr == NULL) {
return; /* out of memory */
}
(*env)->SetObjectField(env, obj, fid_s, jstr);
}
由于多個線程可能同時訪問這個本地方法,上面方法中的代碼很可能會導(dǎo)致混亂秸应,其實沒事虑凛,多個線程計算的ID其實是相同的。
同樣的思想软啼,我們也可以緩存java.lang.String的構(gòu)造方法的ID:
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
jclass stringClass;
jcharArray elemArr;
static jmethodID cid = NULL;
jstring result;
stringClass = (*env)->FindClass(env, "java/lang/String");
if (stringClass == NULL) {
return NULL; /* exception thrown */
}
/* Note that cid is a static variable */
if (cid == NULL) {
/* Get the method ID for the String constructor */
cid = (*env)->GetMethodID(env, stringClass,
"<init>", "([C)V");
if (cid == NULL) {
return NULL; /* exception thrown */
}
}
/* Create a char[] that holds the string characters */
elemArr = (*env)->NewCharArray(env, len);
if (elemArr == NULL) {
return NULL; /* exception thrown */
}
(*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
/* Construct a java.lang.String object */
result = (*env)->NewObject(env, stringClass, cid, elemArr);
/* Free local references */
(*env)->DeleteLocalRef(env, elemArr);
(*env)->DeleteLocalRef(env, stringClass);
return result;
}
當(dāng)MyNewString方法第一次被調(diào)用時桑谍,我們計算java.lang.String的構(gòu)造方法的ID,并存儲在靜態(tài)變量cid中祸挪。
4.4.2 類的靜態(tài)初始化過程中緩存字段和方法ID
我們在使用時緩存字段和方法的ID的話锣披,每次本地方法被調(diào)用時都要檢查ID是否已經(jīng)被緩存。許多情況下贿条,在字段ID和方法ID被使用前就初始化是很方便的雹仿。VM在調(diào)用一個類的方法和字段之前,都會執(zhí)行類的靜態(tài)初始化過程整以,所以在靜態(tài)初始化該類的過程中計算并緩存字段ID和方法ID是個不錯的選擇胧辽。
例如,為了緩存InstanceMethodCall.callback的方法ID悄蕾,我們引入了一個新的本地方法initIDs票顾,這個方法在InstanceMethodCall的靜態(tài)初始化過程中被調(diào)用础浮。代碼如下:
class InstanceMethodCall {
private static native void initIDs();
private native void nativeMethod();
private void callback() {
System.out.println("In Java");
}
public static void main(String args[]) {
InstanceMethodCall c = new InstanceMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary("InstanceMethodCall");
initIDs();
}
}
與4.2節(jié)中的代碼相比,上面這段代碼多了兩行奠骄,initIDs方法簡單地計算并緩存方法ID:
jmethodID MID_InstanceMethodCall_callback;
JNIEXPORT void JNICALL
Java_InstanceMethodCall_initIDs(JNIEnv env, jclass cls)
{
MID_InstanceMethodCall_callback =
(env)->GetMethodID(env, cls, "callback", "()V");
}
VM進(jìn)行靜態(tài)初始化時在調(diào)用任何方法前調(diào)用initIDs豆同,這樣方法ID就被緩存了全局變量中,本地方法的實現(xiàn)就不必再進(jìn)行ID計算:
JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv env, jobject obj)
{
printf("In C\n");
(env)->CallVoidMethod(env, obj,
MID_InstanceMethodCall_callback);
}
4.4.3 兩種緩存ID的方式之間的對比
如果JNI程序員不能控制方法和字段所在的類的源碼的話含鳞,在使用時緩存是個合理的方案影锈。例如在MyNewString當(dāng)中,我們不能在String類中插入一個initIDs方法蝉绷。
比起靜態(tài)初始時緩存來說鸭廷,使用時緩存有一些缺點:
1、 使用時緩存的話熔吗,每次使用時都要檢查一下辆床。
2、 方法ID和字段ID在類被unload時就會失效桅狠,如果你在使用時緩存ID讼载,你必須確保只要本地代碼依賴于這個ID的值,那么這個類不被會unload(下一章演示了如何通過使用JNI函數(shù)創(chuàng)建一個類引用來防止類被unload)中跌。另一方面咨堤,如果緩存發(fā)生在靜態(tài)初始化時,當(dāng)類被unload和reload時漩符,ID會被重新計算一喘。
因此,盡可能在靜態(tài)初始化時緩存字段ID和方法ID嗜暴。
4.5 JNI操作JAVA中的字段和方法時的效率
學(xué)完了如何緩存ID來提高效率后凸克,你可能會對使用JNI訪問java字段和方法的效率不太明白,native/java比起java/native和java/java來的話闷沥,效率如何呢触徐?
當(dāng)然,這取決于VM的實現(xiàn)狐赡。我們不能給出在大范圍的VM上通用的數(shù)據(jù),但我們可以通過分析本地方法回調(diào)java方法和JNI操作字段以及方法的過程來給出一個大致的概念疟丙。
我們從比較java/native和java/java的效率開始颖侄。java/native調(diào)用比java/java要慢,主要有以下幾個原因:
1享郊、 java/native比起JVM內(nèi)部的java/java來說有一個調(diào)用轉(zhuǎn)換過程览祖,在把控制權(quán)和入口切換給本地方法之前,VM必須做一些額外的操作來創(chuàng)建參數(shù)和棧幀炊琉。
2展蒂、 對VM來說又活,對方法調(diào)用進(jìn)行內(nèi)聯(lián)比較容易,而內(nèi)聯(lián)java/native方法要難得多锰悼。
據(jù)我們的估計柳骄,VM進(jìn)行java/native調(diào)用時的消耗是java/java的2~3倍。當(dāng)然VM可以進(jìn)行一些調(diào)整箕般,使用java/native的消耗接近或者等于java/java的消耗耐薯。
技術(shù)上來講,native/java調(diào)用和java/native是相似的丝里。但實際上native/java調(diào)用很少見曲初,VM通常不會優(yōu)化native/java這種回調(diào)方式。多數(shù)VM中杯聚,native/java調(diào)用的消耗可以達(dá)到j(luò)ava/java調(diào)用的10倍臼婆。
使用JNI訪問字段的花費取決于通過JNIEnv進(jìn)行調(diào)用的消耗。以廢棄一個對象引用來說幌绍,本地代碼必須依賴于特定的JNI函數(shù)才能做到颁褂,而這個依賴是必須的,它把本地代碼和VM中對象的內(nèi)部形式很好地隔離開纷捞。