本章主要內容
·? 通過一個實例,介紹JNI技術和在使用中應注意的問題。
本章涉及的源代碼文件名及位置
下面是本章分析的源碼文件名及其位置。
·? MediaScanner.java
framework/base/media/java/src/android/media/MediaScanner.java
·? android_media_MediaScanner.cpp
framework/base/media/jni/MediaScanner.cpp
·? android_media_MediaPlayer.cpp
framework/base/media/jni/android_media_MediaPlayer.cpp
·? AndroidRunTime.cpp
framework/base/core/jni/AndroidRunTime.cpp
·? JNIHelp.c
dalvik/libnativehelper/JNIHelp.c
JNI,是Java Native Interface的縮寫,中文為Java本地調用。通俗地說,JNI是一種技術,通過這種技術可以做到以下兩點:
·? Java程序中的函數可以調用Native語言寫的函數,Native一般指的是C/C++編寫的函數。
·? Native程序中的函數可以調用Java層的函數,也就是在C/C++程序中可以調用Java的函數。
在平臺無關的Java中,為什么要創(chuàng)建一個和Native相關的JNI技術呢?這豈不是破壞了Java的平臺無關特性嗎?本人覺得,JNI技術的推出可能是出于以下幾個方面的考慮:
·? 承載Java世界的虛擬機是用Native語言寫的,而虛擬機又運行在具體平臺上,所以虛擬機本身無法做到平臺無關。然而,有了JNI技術,就可以對Java層屏蔽具體的虛擬機實現上的差異了。這樣,就能實現Java本身的平臺無關特性。其實Java一直在使用JNI技術,只是我們平時較少用到罷了。
·? 早在Java語言誕生前,很多程序都是用Native語言寫的,它們遍布在軟件世界的各個角落。Java出世后,它受到了追捧,并迅速得到發(fā)展,但仍無法對軟件世界徹底改朝換代,于是才有了折中的辦法。既然已經有Native模塊實現了相關功能,那么在Java中通過JNI技術直接使用它們就行了,免得落下重復制造輪子的壞名聲。另外,在一些要求效率和速度的場合還是需要Native語言參與的。
在Android平臺上,JNI就是一座將Native世界和Java世界間的天塹變成通途的橋,來看圖2-1,它展示了Android平臺上JNI所處的位置:
http://wiki.jikexueyuan.com/project/deep-android-v1/images/chapter2/image001.png" alt="image" />
圖2-1? Android平臺中JNI示意圖
由上圖可知,JNI將Java世界和Native世界緊密地聯系在一起了。在Android平臺上盡情使用Java開發(fā)的程序員們不要忘了,如果沒有JNI的支持,我們將寸步難行!
注意,雖然JNI層的代碼是用Native語言寫的,但本書還是把和JNI相關的模塊單獨歸類到JNI層。
俗話說,百聞不如一見,就來見識一下JNI技術吧。
?
2.2 ?通過實例學習JNI
初次接觸JNI,感覺最神奇的就是,Java竟然能夠調用Native的函數,可它是怎么做到的呢?網上有很多介紹JNI的資料。由于Android大量使用了JNI技術,本節(jié)就將通過源碼中的一處實例,來學習相關的知識,并了解它是如何調用Native的函數的。
這個例子,是和MediaScanner相關的。在本書的最后一章,會詳細分析它的工作原理,這里先看和JNI相關的部分,如圖2-2所示:
http://wiki.jikexueyuan.com/project/deep-android-v1/images/chapter2/image002.png" alt="image" />
圖2-2? MediaScanner和它的JNI
將圖2-2與圖2-1結合來看,可以知道:
·? Java世界對應的是MediaScanner,而這個MediaScanner類有一些函數是需要由Native層實現的。
·? JNI層對應的是libmedia_jni.so。media_jni是JNI庫的名字,其中,下劃線前的“media”是Native層庫的名字,這里就是libmedia庫。下劃線后的”jni“表示它是一個JNI庫。注意,JNI庫的名字可以隨便取,不過Android平臺基本上都采用“l(fā)ib模塊名_jni.so”的命名方式。
·? Native層對應的是libmedia.so,這個庫完成了實際的功能。
·? MediaScanner將通過JNI庫libmedia_jni.so和Native的libmedia.so交互。
從上面的分析中還可知道:
·? JNI層必須實現為動態(tài)庫的形式,這樣Java虛擬機才能加載它并調用它的函數。
下面來看MediaScanner。
MediaScanner是Android平臺中多媒體系統的重要組成部分,它的功能是掃描媒體文件,得到諸如歌曲時長、歌曲作者等媒體信息,并將它們存入到媒體數據庫中,供其他應用程序使用。
來看MediaScanner(簡稱MS)的源碼,這里將提取出和JNI有關的部分,其代碼如下所示:
[-->MediaScanner.java]
public class MediaScanner
{
static{ static語句
??? /*
①加載對應的JNI庫,media_jni是JNI庫的名字。實際加載動態(tài)庫的時候會拓展成
libmedia_jni.so,在Windows平臺上將拓展為media_jni.dll。
*/
???????System.loadLibrary("media_jni");
???????native_init();//調用native_init函數
??? }
.......
//非native函數
publicvoid scanDirectories(String[] directories, String volumeName){
? ......
}
?
//②聲明一個native函數。native為Java的關鍵字,表示它將由JNI層完成。
privatestatic native final void native_init();
??? ......
privatenative void processFile(String path, String mimeType,
?MediaScannerClient client);
??? ......
}
·? 上面代碼中列出了兩個比較重要的要點:
前面說過,如Java要調用Native函數,就必須通過一個位于JNI層的動態(tài)庫才能做到。顧名思義,動態(tài)庫就是運行時加載的庫,那么是什么時候,在什么地方加載這個庫呢?
這個問題沒有標準答案,原則上是在調用native函數前,任何時候、任何地方加載都可以。通行的做法是,在類的static語句中加載,通過調用System.loadLibrary方法就可以了。這一點,在上面的代碼中也見到了,我們以后就按這種方法編寫代碼即可。另外,System.loadLibrary函數的參數是動態(tài)庫的名字,即media_jni。系統會自動根據不同的平臺拓展成真實的動態(tài)庫文件名,例如在Linux系統上會拓展成libmedia_jni.so,而在Windows平臺上則會拓展成media_jni.dll。
解決了JNI庫加載的問題,再來來看第二個關鍵點。
從上面代碼中可以發(fā)現,native_init和processFile函數前都有Java的關鍵字native,它表示這兩個函數將由JNI層來實現。
Java層的分析到此結束。JNI技術也很照顧Java程序員,只要完成下面兩項工作就可以使用JNI了,它們是:
·? 加載對應的JNI庫。
·? 聲明由關鍵字native修飾的函數。
所以對于Java程序員來說,使用JNI技術真的是太容易了。不過JNI層可沒這么輕松,下面來看MS的JNI層分析。
MS的JNI層代碼在android_media_MediaScanner.cpp中,如下所示:
[-->android_media_MediaScanner.cpp]
//①這個函數是native_init的JNI層實現。
static void?android_media_MediaScanner_native_init(JNIEnv *env)
{
????jclass clazz;
?
??? clazz= env->FindClass("android/media/MediaScanner");
??? ......
???fields.context = env->GetFieldID(clazz, "mNativeContext","I");
......
return;
}
?
//這個函數是processFile的JNI層實現。
static void android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,
jstring path, jstring mimeType, jobject client)
{
??? MediaScanner*mp = (MediaScanner *)env->GetIntField(thiz, fields.context);
??? ......
??? constchar *pathStr = env->GetStringUTFChars(path, NULL);
? ??......
??? if(mimeType) {
???????env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
??? }
}
上面是MS的JNI層代碼,不知道讀者看了以后是否會產生些疑惑?
我想,最大的疑惑可能是,怎么會知道Java層的native_init函數對應的是JNI層的android_media_MediaScanner_native_init函數呢?下面就來回答這個問題。
正如代碼中注釋的那樣,native_init函數對應的JNI函數是android_media_MediaScanner_native_init,可是細心的讀者可能要問了,你怎么知道native_init函數對應的是這個android_media_MediaScanner_native_init,而不是其他的呢?莫非是根據函數的名字?
大家知道,native_init函數位于android.media這個包中,它的全路徑名應該是android.media.MediaScanner.native_init,而JNI層函數的名字是android_media_MediaScanner_native_init。因為在Native語言中,符號“.”有著特殊的意義,所以JNI層需要把“.”換成“_”。也就是通過這種方式,native_init找到了自己JNI層的本家兄弟android.media.MediaScanner.native_init。
上面的問題其實討論的是JNI函數的注冊問題,“注冊”之意就是將Java層的native函數和JNI層對應的實現函數關聯起來,有了這種關聯,調用Java層的native函數時,就能順利轉到JNI層對應的函數執(zhí)行了。而JNI函數的注冊實際上有兩種方法,下面分別做介紹。
我們從網上找到的與JNI有的關資料,一般都會介紹如何使用這種方法完成JNI函數的注冊,這種方法就是根據函數名來找對應的JNI函數。這種方法需要Java的工具程序javah參與,整體流程如下:
·? 先編寫Java代碼,然后編譯生成.class文件。
·? 使用Java的工具程序javah,如javah–o output packagename.classname ,這樣它會生成一個叫output.h的JNI層頭文件。其中packagename.classname是Java代碼編譯后的class文件,而在生成的output.h文件里,聲明了對應的JNI層函數,只要實現里面的函數即可。
這個頭文件的名字一般都會使用packagename_class.h的樣式,例如MediaScanner對應的JNI層頭文件就是android_media_MediaScanner.h。下面,來看這種方式生成的頭文件:
[-->android_media_MediaScanner.h::樣例文件]
/* DO NOT EDIT THIS FILE - it is machinegenerated */
#include <jni.h>? //必須包含這個頭文件,否則編譯通不過
/* Header for class android_media_MediaScanner*/
?
#ifndef _Included_android_media_MediaScanner
#define _Included_android_media_MediaScanner
#ifdef __cplusplus
extern "C" {
#endif
...... 略去一部分注釋內容
//processFile的JNI函數
JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile
? ?????????????????(JNIEnv *, jobject, jstring,jstring, jobject);
?
......//略去一部分注釋內容
//native_init對應的JNI函數
JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init
? (JNIEnv*, jclass);
?
#ifdef __cplusplus
}
#endif
#endif
從上面代碼中可以發(fā)現,native_init和processFile的JNI層函數被聲明成:
//Java層函數名中如果有一個”_”的話,轉換成JNI后就變成了”_l”。
JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init
JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile
需解釋一下,靜態(tài)方法中native函數是如何找到對應的JNI函數的。其實,過程非常簡單:
·? 當Java層調用native_init函數時,它會從對應的JNI庫Java_android_media_MediaScanner_native_linit,如果沒有,就會報錯。如果找到,則會為這個native_init和Java_android_media_MediaScanner_native_linit建立一個關聯關系,其實就是保存JNI層函數的函數指針。以后再調用native_init函數時,直接使用這個函數指針就可以了,當然這項工作是由虛擬機完成的。
從這里可以看出,靜態(tài)方法就是根據函數名來建立Java函數和JNI函數之間的關聯關系的,它要求JNI層函數的名字必須遵循特定的格式。這種方法也有幾個弊端,它們是:
·? 需要編譯所有聲明了native函數的Java類,每個生成的class文件都得用javah生成一個頭文件。
·? javah生成的JNI層函數名特別長,書寫起來很不方便。
·? 初次調用native函數時要根據函數名字搜索對應的JNI層函數來建立關聯關系,這樣會影響運行效率。
有什么辦法可以克服上面三種弊端嗎?根據上面的介紹,Java native函數是通過函數指針來和JNI層函數建立關聯關系的。如果直接讓native函數知道JNI層對應函數的函數指針,不就萬事大吉了嗎?這就是下面要介紹的第二種方法:動態(tài)注冊法。
既然Java native函數數和JNI函數是一一對應的,那么是不是會有一個結構來保存這種關聯關系呢?答案是肯定的。在JNI技術中,用來記錄這種一一對應關系的,是一個叫JNINativeMethod的結構,其定義如下:
typedef struct {
?? //Java中native函數的名字,不用攜帶包的路徑。例如“native_init“。
constchar* name; ???
//Java函數的簽名信息,用字符串表示,是參數類型和返回值類型的組合。
?? ?const char* signature;
???void*?????? fnPtr; ?//JNI層對應函數的函數指針,注意它是void*類型。
} JNINativeMethod;
應該如何使用這個結構體呢?來看MediaScanner JNI層是如何做的,代碼如下所示:
[-->android_media_MediaScanner.cpp]
//定義一個JNINativeMethod數組,其成員就是MS中所有native函數的一一對應關系。
static JNINativeMethod gMethods[] = {
??? ......
{
"processFile" //Java中native函數的函數名。
//processFile的簽名信息,簽名信息的知識,后面再做介紹。
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",???
?(void*)android_media_MediaScanner_processFile //JNI層對應函數指針。
},
?......
?
{
"native_init",???????
"()V",?????????????????????
(void *)android_media_MediaScanner_native_init
},
? ......
};
//注冊JNINativeMethod數組
int register_android_media_MediaScanner(JNIEnv*env)
{
?? //調用AndroidRuntime的registerNativeMethods函數,第二個參數表明是Java中的哪個類
??? returnAndroidRuntime::registerNativeMethods(env,
???????????????"android/media/MediaScanner", gMethods, NELEM(gMethods));
}
AndroidRunTime類提供了一個registerNativeMethods函數來完成注冊工作,下面看registerNativeMethods的實現,代碼如下:
[-->AndroidRunTime.cpp]
int AndroidRuntime::registerNativeMethods(JNIEnv*env,
??? constchar* className, const JNINativeMethod* gMethods, int numMethods)
{
??? //調用jniRegisterNativeMethods函數完成注冊
??? returnjniRegisterNativeMethods(env, className, gMethods, numMethods);
}
其中jniRegisterNativeMethods是Android平臺中,為了方便JNI使用而提供的一個幫助函數,其代碼如下所示:
[-->JNIHelp.c]
int jniRegisterNativeMethods(JNIEnv* env, constchar* className,
??? ??????????????????????????????constJNINativeMethod* gMethods, int numMethods)
{
??? jclassclazz;
??? clazz= (*env)->FindClass(env, className);
......
//實際上是調用JNIEnv的RegisterNatives函數完成注冊的
??? if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
???????return -1;
??? }
??? return0;
}
wow,好像很麻煩??!其實動態(tài)注冊的工作,只用兩個函數就能完成??偨Y如下:
/*
env指向一個JNIEnv結構體,它非常重要,后面會討論它。classname為對應的Java類名,由于
JNINativeMethod中使用的函數名并非全路徑名,所以要指明是哪個類。
*/
jclass clazz = ?(*env)->FindClass(env, className);
//調用JNIEnv的RegisterNatives函數,注冊關聯關系。
(*env)->RegisterNatives(env, clazz, gMethods,numMethods);
所以,在自己的JNI層代碼中使用這種方法,就可以完成動態(tài)注冊了。這里還有一個很棘手的問題:這些動態(tài)注冊的函數在什么時候、什么地方被誰調用呢?好了,不賣關子了,直接給出該問題的答案:
·? 當Java層通過System.loadLibrary加載完JNI動態(tài)庫后,緊接著會查找該庫中一個叫JNI_OnLoad的函數,如果有,就調用它,而動態(tài)注冊的工作就是在這里完成的。
所以,如果想使用動態(tài)注冊方法,就必須要實現JNI_OnLoad函數,只有在這個函數中,才有機會完成動態(tài)注冊的工作。靜態(tài)注冊則沒有這個要求,可我建議讀者也實現這個JNI_OnLoad函數,因為有一些初始化工作是可以在這里做的。
那么,libmedia_jni.so的JNI_OnLoad函數是在哪里實現的呢?由于多媒體系統很多地方都使用了JNI,所以碼農把它放到android_media_MediaPlayer.cpp中了,代碼如下所示:
[-->android_media_MediaPlayer.cpp]
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
?? //該函數的第一個參數類型為JavaVM,這可是虛擬機在JNI層的代表喔,每個Java進程只有一個
? //這樣的JavaVM
???JNIEnv* env = NULL;
??? jintresult = -1;
?
??? if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
?????? ??gotobail;
??? }
?? ?...... //動態(tài)注冊MediaScanner的JNI函數。
??? if(register_android_media_MediaScanner(env) < 0) {
????????goto bail;
}
......
returnJNI_VERSION_1_4;//必須返回這個值,否則會報錯。
}
JNI函數注冊的內容介紹完了。下面來關注JNI技術中其他的幾個重要部分。
JNI層代碼中一般要包含jni.h這個頭文件。Android源碼中提供了一個幫助頭文件JNIHelp.h,它內部其實就包含了jni.h,所以我們在自己的代碼中直接包含這個JNIHelp.h即可。
通過前面的分析,解決了JNI函數的注冊問題。下面來研究數據類型轉換的問題。
在Java中調用native函數傳遞的參數是Java數據類型,那么這些參數類型到了JNI層會變成什么呢?
Java數據類型分為基本數據類型和引用數據類型兩種,JNI層也是區(qū)別對待這二者的。先來看基本數據類型的轉換。
基本類型的轉換很簡單,可用表2-1表示:
表2-1? 基本數據類型轉換關系表
|
Java |
Native類型 |
符號屬性 |
字長 |
|
boolean |
jboolean |
無符號 |
8位 |
|
byte |
jbyte |
無符號 |
8位 |
|
char |
jchar |
無符號 |
16位 |
|
short |
jshort |
有符號 |
16位 |
|
int |
jint |
有符號 |
32位 |
|
long |
jlong |
有符號 |
64位 |
|
float |
jfloat |
有符號 |
32位 |
|
double |
jdouble |
有符號 |
64位 |
上面列出了Java基本數據類型和JNI層數據類型對應的轉換關系,非常簡單。不過,應務必注意,轉換成Native類型后對應數據類型的字長,例如jchar在Native語言中是16位,占兩個字節(jié),這和普通的char占一個字節(jié)的情況完全不一樣。
接下來看Java引用數據類型的轉換。
引用數據類型的轉換如表2-2所示:
表2-2? Java引用數據類型轉換關系表
|
Java引用類型 |
Native類型 |
Java引用類型 |
Native類型 |
|
All objects |
jobject |
char[] |
jcharArray |
|
java.lang.Class實例 |
jclass |
short[] |
jshortArray |
|
java.lang.String實例 |
jstring |
int[] |
jintArray |
|
Object[] |
jobjectArray |
long[] |
jlongArray |
|
boolean[] |
jbooleanArray |
float[] |
floatArray |
|
byte[] |
jbyteArray |
double[] |
jdoubleArray |
|
java.lang.Throwable實例 |
jthrowable |
? |
? |
由上表可知:
·? 除了Java中基本數據類型的數組、Class、String和Throwable外,其余所有Java對象的數據類型在JNI中都用jobject表示。
這一點太讓人驚訝了!看processFile這個函數:
//Java層processFile有三個參數。
processFile(String path, StringmimeType,MediaScannerClient client);
//JNI層對應的函數,最后三個參數和processFile的參數對應。
android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,
jstring path, jstring mimeType, jobject client)
從上面這段代碼中可以發(fā)現:
·? Java的String類型在JNI層對應為jstring。
·? Java的MediaScannerClient類型在JNI層對應為jobject。
如果對象類型都用jobject表示,就好比是Native層的void*類型一樣,對碼農來說,是完全透明的。既然是透明的,那該如何使用和操作它們呢?在回答這個問題之前,再來仔細看看上面那個android_media_MediaScanner_processFile函數,代碼如下:
/*
Java中的processFile只有三個參數,為什么JNI層對應的函數會有五個參數呢?第一個參數中的JNIEnv是什么?稍后介紹。第二個參數jobject代表Java層的MediaScanner對象,它表示
是在哪個MediaScanner對象上調用的processFile。如果Java層是static函數的話,那么
這個參數將是jclass,表示是在調用哪個Java Class的靜態(tài)函數。
*/
android_media_MediaScanner_processFile(JNIEnv*env,
jobject thiz,
jstring path, jstring mimeType, jobject client)
上面的代碼,引出了下面幾節(jié)的主角JNIEnv。
JNIEnv是一個和線程相關的,代表JNI環(huán)境的結構體,圖2-3展示了JNIEnv的內部結構:
http://wiki.jikexueyuan.com/project/deep-android-v1/images/chapter2/image003.png" alt="image" />
圖2-3? JNIEnv內部結構簡圖
從上圖可知,JNIEnv實際上就是提供了一些JNI系統函數。通過這些函數可以做到:
·? 調用Java的函數。
·? 操作jobject對象等很多事情。
后面小節(jié)中將具體介紹怎么使用JNIEnv中的函數。這里,先介紹一個關于JNIEnv的重要知識點。
上面提到說JNIEnv,是一個和線程有關的變量。也就是說,線程A有一個JNIEnv,線程B有一個JNIEnv。由于線程相關,所以不能在線程B中使用線程A的JNIEnv結構體。讀者可能會問,JNIEnv不都是native函數轉換成JNI層函數后由虛擬機傳進來的嗎?使用傳進來的這個JNIEnv總不會錯吧?是的,在這種情況下使用當然不會出錯。不過當后臺線程收到一個網絡消息,而又需要由Native層函數主動回調Java層函數時,JNIEnv是從何而來呢?根據前面的介紹可知,我們不能保存另外一個線程的JNIEnv結構體,然后把它放到后臺線程中來用。這該如何是好?
還記得前面介紹的那個JNI_OnLoad函數嗎?它的第一個參數是JavaVM,它是虛擬機在JNI層的代表,代碼如下所示:
//全進程只有一個JavaVM對象,所以可以保存,任何地方使用都沒有問題。
jint JNI_OnLoad(JavaVM* vm, void* reserved)
正如上面代碼所說,不論進程中有多少個線程,JavaVM卻是獨此一份,所以在任何地方都可以使用它。那么,JavaVM和JNIEnv又有什么關系呢?答案如下:
·? 調用JavaVM的AttachCurrentThread函數,就可得到這個線程的JNIEnv結構體。這樣就可以在后臺線程中回調Java函數了。
·? 另外,后臺線程退出前,需要調用JavaVM的DetachCurrentThread函數來釋放對應的資源。
再來看JNIEnv的作用。
前面提到過一個問題,即Java的引用類型除了少數幾個外,最終在JNI層都用jobject來表示對象的數據類型,那么該如何操作這個jobject呢?
從另外一個角度來解釋這個問題。一個Java對象是由什么組成的?當然是它的成員變量和成員函數了。那么,操作jobject的本質就應當是操作這些對象的成員變量和成員函數。所以應先來看與成員變量及成員函數有關的內容。
我們知道,成員變量和成員函數是由類定義的,它是類的屬性,所以在JNI規(guī)則中,用jfieldID 和jmethodID 來表示Java類的成員變量和成員函數,它們通過JNIEnv的下面兩個函數可以得到:
jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);
其中,jclass代表Java類,name表示成員函數或成員變量的名字,sig為這個函數和變量的簽名信息。如前所示,成員函數和成員變量都是類的信息,這兩個函數的第一個參數都是jclass。
MS中是怎么使用它們的呢?來看代碼,如下所示:
[-->android_media_MediaScanner.cpp::MyMediaScannerClient構造函數]
?MyMediaScannerClient(JNIEnv *env, jobjectclient)......
{
?//先找到android.media.MediaScannerClient類在JNI層中對應的jclass實例。
jclass mediaScannerClientInterface =
env->FindClass("android/media/MediaScannerClient");
?//取出MediaScannerClient類中函數scanFile的jMethodID。
mScanFileMethodID = env->GetMethodID(
mediaScannerClientInterface, "scanFile",
? ?????????????????????????"(Ljava/lang/String;JJ)V");
?//取出MediaScannerClient類中函數handleStringTag的jMethodID。
?mHandleStringTagMethodID = env->GetMethodID(
mediaScannerClientInterface,"handleStringTag",
???????? ????????????????????"(Ljava/lang/String;Ljava/lang/String;)V");
? ......
}
在上面代碼中,將scanFile和handleStringTag函數的jmethodID保存為MyMediaScannerClient的成員變量。為什么這里要把它們保存起來呢?這個問題涉及一個事關程序運行效率的知識點:
·? 如果每次操作jobject前都去查詢jmethoID或jfieldID的話將會影響程序運行的效率。所以我們在初始化的時候,就可以取出這些ID并保存起來以供后續(xù)使用。
取出jmethodID后,又該怎么用它呢?
下面再看一個例子,其代碼如下所示:
[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile]
?virtualbool scanFile(const char* path, long long lastModified,
long long fileSize)
??? {
???????jstring pathStr;
??????? if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
???????
/*
調用JNIEnv的CallVoidMethod函數,注意CallVoidMethod的參數:
第一個是代表MediaScannerClient的jobject對象,
第二個參數是函數scanFile的jmethodID,后面是Java中scanFile的參數。
*/
???????mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,
lastModified, fileSize);
?
???????mEnv->DeleteLocalRef(pathStr);
???????return (!mEnv->ExceptionCheck());
}
明白了,通過JNIEnv輸出的CallVoidMethod,再把jobject、jMethodID和對應參數傳進去,JNI層就能夠調用Java對象的函數了!
實際上JNIEnv輸出了一系列類似CallVoidMethod的函數,形式如下:
NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)。
其中type是對應Java函數的返回值類型,例如CallIntMethod、CallVoidMethod等。
上面是針對非static函數的,如果想調用Java中的static函數,則用JNIEnv輸出的CallStatic<Type>Method系列函數。
現在,我們已了解了如何通過JNIEnv操作jobject的成員函數,那么怎么通過jfieldID操作jobject的成員變量呢?這里,直接給出整體解決方案,如下所示:
//獲得fieldID后,可調用Get<type>Field系列函數獲取jobject對應成員變量的值。
NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)
//或者調用Set<type>Field系列函數來設置jobject對應成員變量的值。
void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)
//下面我們列出一些參加的Get/Set函數。
GetObjectField()???????? SetObjectField()
GetBooleanField()?? ????? SetBooleanField()
GetByteField()?????????? SetByteField()
GetCharField()?????????? SetCharField()
GetShortField()????????? SetShortField()
GetIntField()??????????? SetIntField()
GetLongField()?????????? SetLongField()
GetFloatField()????????? SetFloatField()
GetDoubleField() ???????????????? SetDoubleField()
通過本節(jié)的介紹,相信讀者已了解jfieldID和jmethodID的作用,也知道如何通過JNIEnv的函數來操作jobject了。雖然jobject是透明的,但有了JNIEnv的幫助,還是能輕松操作jobject背后的實際對象了。
Java中的String也是引用類型,不過由于它的使用非常頻繁,所以在JNI規(guī)范中單獨創(chuàng)建了一個jstring類型來表示Java中的String類型。雖然jstring是一種獨立的數據類型,但是它并沒有提供成員函數供操作。相比而言,C++中的string類就有自己的成員函數了。那么該怎么操作jstring呢?還是得依靠JNIEnv提供的幫助。這里看幾個有關jstring的函數:
·? 調用JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len),可以從Native的字符串得到一個jstring對象。其實,可以把一個jstring對象看成是Java中String對象在JNI層的代表,也就是說,jstring就是一個Java String。但由于Java String存儲的是Unicode字符串,所以NewString函數的參數也必須是Unicode字符串。
·? 調用JNIEnv的NewStringUTF將根據Native的一個UTF-8字符串得到一個jstring對象。在實際工作中,這個函數用得最多。
·? 上面兩個函數將本地字符串轉換成了Java的String對象,JNIEnv還提供了GetStringChars和GetStringUTFChars函數,它們可以將Java String對象轉換成本地字符串。其中GetStringChars得到一個Unicode字符串,而GetStringUTFChars得到一個UTF-8字符串。
·? 另外,如果在代碼中調用了上面幾個函數,在做完相關工作后,就都需要調用ReleaseStringChars或ReleaseStringUTFChars函數對應地釋放資源,否則會導致JVM內存泄露。這一點和jstring的內部實現有關系,讀者寫代碼時務必注意這個問題。
為了加深印象,來看processFile是怎么做的:
[-->android_media_MediaScanner.cpp]
static void
android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
???MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz,fields.context);
......
//調用JNIEnv的GetStringUTFChars得到本地字符串pathStr
??? constchar *pathStr = env->GetStringUTFChars(path, NULL);
......
//使用完后,必須調用ReleaseStringUTFChars釋放資源
???env->ReleaseStringUTFChars(path, pathStr);
??? ......
}
先來看動態(tài)注冊中的一段代碼:
tatic JNINativeMethod gMethods[] = {
??? ......
{
"processFile"
//processFile的簽名信息,這么長的字符串,是什么意思?
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",???
?(void*)android_media_MediaScanner_processFile
},
? ......
}
上面代碼中的JNINativeMethod已經見過了,不過其中那個很長的字符串"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V"是什么意思呢?
根據前面的介紹可知,它是Java中對應函數的簽名信息,由參數類型和返回值類型共同組成。不過為什么需要這個簽名信息呢?
·? 這個問題的答案比較簡單。因為Java支持函數重載,也就是說,可以定義同名但不同參數的函數。但僅僅根據函數名,是沒法找到具體函數的。為了解決這個問題,JNI技術中就使用了參數類型和返回值類型的組合,作為一個函數的簽名信息,有了簽名信息和函數名,就能很順利地找到Java中的函數了。
JNI規(guī)范定義的函數簽名信息看起來很別扭,不過習慣就好了。它的格式是:
(參數1類型標示參數2類型標示...參數n類型標示)返回值類型標示。
來看processFile的例子:
Java中函數定義為void processFile(String path, String mimeType)
對應的JNI函數簽名就是
(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V
?其中,括號內是參數類型的標示,最右邊是返回值類型的標示,void類型對應的標示是V。
?當參數的類型是引用類型時,其格式是”L包名;”,其中包名中的”.”換成”/”。上面例子中的
Ljava/lang/String;表示是一個Java String類型。
函數簽名不僅看起來麻煩,寫起來更麻煩,稍微寫錯一個標點就會導致注冊失敗。所以,在具體編碼時,讀者可以定義字符串宏,這樣改起來也方便。
表2-3是常見的類型標示:
表2-3? 類型標示示意表
|
類型標示 |
Java類型 |
類型標示 |
Java類型 |
|
Z |
boolean |
F |
float |
|
B |
byte |
D |
double |
|
C |
char |
L/java/langaugeString; |
String |
|
S |
short |
[I |
int[] |
|
I |
int |
[L/java/lang/object; |
Object[] |
|
J |
long |
? |
? |
上面列出了一些常用的類型標示。請讀者注意,如果Java類型是數組,則標示中會有一個“[”,另外,引用類型(除基本類型的數組外)的標示最后都有一個“;”。
再來看一個小例子,如表2-4所示:
表2-4? 函數簽名小例子
|
函數簽名 |
Java函數 |
|
“()Ljava/lang/String;” |
String f() |
|
“(ILjava/lang/Class;)J” |
long f(int i, Class c) |
|
“([B)V” |
void f(byte[] bytes) |
請讀者結合表2-3和表2-4左欄的內容寫出對應的Java函數。
雖然函數簽名信息很容易寫錯,但Java提供一個叫javap的工具能幫助生成函數或變量的簽名信息,它的用法如下:
javap –s -p xxx。其中xxx為編譯后的class文件,s表示輸出內部數據類型的簽名信息,p表示打印所有函數和成員的簽名信息,而默認只會打印public成員和函數的簽名信息。
有了javap,就不用死記硬背上面的類型標示了。
我們知道,Java中創(chuàng)建的對象最后是由垃圾回收器來回收和釋放內存的,可它對JNI有什么影響呢?下面看一個例子:
[-->垃圾回收例子]
static jobject save_thiz = NULL; //定義一個全局的jobject
static void
android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path,
?jstringmimeType, jobject client)
{
? ......
? //保存Java層傳入的jobject對象,代表MediaScanner對象
save_thiz = thiz;
......
return;
}
//假設在某個時間,有地方調用callMediaScanner函數
void callMediaScanner()
{
? //在這個函數中操作save_thiz,會有問題嗎?
}
上面的做法肯定會有問題,因為和save_thiz對應的Java層中的MediaScanner很有可能已經被垃圾回收了,也就是說,save_thiz保存的這個jobject可能是一個野指針,如使用它,后果會很嚴重。
可能有人要問,將一個引用類型進行賦值操作,它的引用計數不會增加嗎?而垃圾回收機制只會保證那些沒有被引用的對象才會被清理。問得對,但如果在JNI層使用下面這樣的語句,是不會增加引用計數的。
save_thiz = thiz; //這種賦值不會增加jobject的引用計數。
那該怎么辦?不必擔心,JNI規(guī)范已很好地解決了這一問題,JNI技術一共提供了三種類型的引用,它們分別是:
·? Local Reference:本地引用。在JNI層函數中使用的非全局引用對象都是Local Reference。它包括函數調用時傳入的jobject、在JNI層函數中創(chuàng)建的jobject。LocalReference最大的特點就是,一旦JNI層函數返回,這些jobject就可能被垃圾回收。
·? Global Reference:全局引用,這種對象如不主動釋放,就永遠不會被垃圾回收。
·? Weak Global Reference:弱全局引用,一種特殊的GlobalReference,在運行過程中可能會被垃圾回收。所以在程序中使用它之前,需要調用JNIEnv的IsSameObject判斷它是不是被回收了。
平時用得最多的是Local Reference和Global Reference,下面看一個實例,代碼如下所示:
[-->android_media_MediaScanner.cpp::MyMediaScannerClient構造函數]
?MyMediaScannerClient(JNIEnv *env, jobjectclient)
???????:?? mEnv(env),
? ??????//調用NewGlobalRef創(chuàng)建一個GlobalReference,這樣mClient就不用擔心被回收了。
???????????mClient(env->NewGlobalRef(client)),
???????????mScanFileMethodID(0),
???????????mHandleStringTagMethodID(0),
???????????mSetMimeTypeMethodID(0)
{
? ......
}
//析構函數
virtual ~MyMediaScannerClient()
{
??mEnv->DeleteGlobalRef(mClient);//調用DeleteGlobalRef釋放這個全局引用。
?}
每當JNI層想要保存Java層中的某個對象時,就可以使用Global Reference,使用完后記住釋放它就可以了。這一點很容易理解。下面要講有關LocalReference的一個問題,還是先看實例,代碼如下所示:
[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile]
?virtualbool scanFile(const char* path, long long lastModified,
long long fileSize)
{
?? jstringpathStr;
?? //調用NewStringUTF創(chuàng)建一個jstring對象,它是Local Reference類型。
?? if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
??????? //調用Java的scanFile函數,把這個jstring傳進去
???????mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,
lastModified, fileSize);
???? /*
???? ?根據LocalReference的說明,這個函數返回后,pathStr對象就會被回收。所以
????? 下面這個DeleteLocalRef調用看起來是多余的,其實不然,這里解釋一下原因:
1)如果不調用DeleteLocalRef,pathStr將在函數返回后被回收。
2)如果調用DeleteLocalRef的話,pathStr會立即被回收。這兩者看起來沒什么區(qū)別,
不過代碼要是像下面這樣的話,虛擬機的內存就會被很快被耗盡:
????? for(i