本章主要內容html
· 經過一個實例,介紹JNI技術和在使用中應注意的問題。java
本章涉及的源代碼文件名稱及位置android
如下是本章分析的源代碼文件名稱及其位置。數據庫
· MediaScanner.java數組
framework/base/media/java/src/android/media/MediaScanner.java網絡
· android_media_MediaScanner.cpp函數
framework/base/media/jni/MediaScanner.cpp工具
· android_media_MediaPlayer.cpppost
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中,爲何要建立一個和Native相關的JNI技術呢?這豈不是破壞了Java的平臺無關特性嗎?本人認爲,JNI技術的推出多是出於如下幾個方面的考慮:
· 承載Java世界的虛擬機是用Native語言寫的。而虛擬機又執行在詳細平臺上,因此虛擬機自己沒法作到平臺無關。然而。有了JNI技術,就可以對Java層屏蔽詳細的虛擬機實現上的差別了。這樣,就能實現Java自己的平臺無關特性。
事實上Java一直在使用JNI技術,僅僅是咱們平時較少用到罷了。
· 早在Java語言誕生前,很是多程序都是用Native語言寫的,它們遍及在軟件世界的各個角落。Java出世後,它受到了追捧,並迅速獲得發展,但仍沒法對軟件世界完全改朝換代。因而纔有了折中的辦法。既然已經有Native模塊實現了相關功能。那麼在Java中經過JNI技術直接使用它們就能夠了,省得落下反複製造輪子的壞名聲。另外,在一些要求效率和速度的場合仍是需要Native語言參與的。
在Android平臺上,JNI就是一座將Native世界和Java世界間的天塹變成通途的橋。來看圖2-1,它展現了Android平臺上JNI所處的位置:
圖2-1 Android平臺中JNI示意圖
由上圖可知。JNI將Java世界和Native世界緊密地聯繫在一塊兒了。在Android平臺上盡情使用Java開發的程序猿們不要忘了。假設沒有JNI的支持,咱們將步履維艱!
注意,儘管JNI層的代碼是用Native語言寫的。但本書仍是把和JNI相關的模塊單獨歸類到JNI層。
俗話說,百聞不如一見,就來見識一下JNI技術吧。
2.2 經過實例學習JNI
初次接觸JNI,感受最奇妙的就是。Java竟然可以調用Native的函數,可它是怎麼作到的呢?網上有很是多介紹JNI的資料。由於Android大量使用了JNI技術。本節就將經過源代碼中的一處實例。來學習相關的知識。並瞭解它是怎樣調用Native的函數的。
這個樣例,是和MediaScanner相關的。
在本書的最後一章,會詳細分析它的工做原理。這裏先看和JNI相關的部分,如圖2-2所看到的:
圖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平臺基本上都採用「lib模塊名_jni.so」的命名方式。
· Native層相應的是libmedia.so。這個庫完畢了實際的功能。
· MediaScanner將經過JNI庫libmedia_jni.so和Native的libmedia.so交互。
從上面的分析中還可知道:
· JNI層必須實現爲動態庫的形式。這樣Java虛擬機才幹載入它並調用它的函數。
如下來看MediaScanner。
MediaScanner是Android平臺中多媒體系統的重要組成部分。它的功能是掃描媒體文件。獲得諸如歌曲時長、歌曲做者等媒體信息,並將它們存入到媒體數據庫中,供其它應用程序使用。
來看MediaScanner(簡稱MS)的源代碼,這裏將提取出和JNI有關的部分。其代碼例如如下所看到的:
[-->MediaScanner.java]
public class MediaScanner
{
static{ static語句
/*
①載入相應的JNI庫。media_jni是JNI庫的名字。實際載入動態庫的時候會拓展成
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的keyword,表示它將由JNI層完畢。
privatestatic native final void native_init();
......
privatenative void processFile(String path, String mimeType,
MediaScannerClient client);
......
}
· 上面代碼中列出了兩個比較重要的要點:
前面說過,如Java要調用Native函數。就必須經過一個位於JNI層的動態庫才幹作到。顧名思義,動態庫就是執行時載入的庫,那麼是何時。在什麼地方載入這個庫呢?
這個問題沒有標準答案,原則上是在調用native函數前,不論何時、不論什麼地方載入都可以。
通行的作法是。在類的static語句中載入,經過調用System.loadLibrary方法就可以了。
這一點。在上面的代碼中也見到了,咱們之後就按這樣的方法編寫代碼就能夠。另外,System.loadLibrary函數的參數是動態庫的名字。即media_jni。系統會本身主動依據不一樣的平臺拓展成真實的動態庫文件名稱,好比在Linux系統上會拓展成libmedia_jni.so。而在Windows平臺上則會拓展成media_jni.dll。
攻克了JNI庫載入的問題,再來來看第二個關鍵點。
從上面代碼中可以發現,native_init和processFile函數前都有Java的keywordnative,它表示這兩個函數將由JNI層來實現。
Java層的分析到此結束。
JNI技術也很是照應Java程序猿。僅僅要完畢如下兩項工做就可以使用JNI了,它們是:
· 載入相應的JNI庫。
· 聲明由keywordnative修飾的函數。
因此對於Java程序猿來講。使用JNI技術真的是太easy了。只是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層相應的函數執行了。而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
從上面代碼中可以發現。native_init和processFile的JNI層函數被聲明成:
//Java層函數名中假設有一個」_」的話,轉換成JNI後就變成了」_l」。
JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init
JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile
需解釋一下,靜態方法中native函數是怎樣找到相應的JNI函數的。事實上。過程很是easy:
· 當Java層調用native_init函數時。它會從相應的JNI庫Java_android_media_MediaScanner_native_linit。假設沒有。就會報錯。假設找到,則會爲這個native_init和Java_android_media_MediaScanner_native_linit創建一個關聯關係。事實上就是保存JNI層函數的函數指針。之後再調用native_init函數時,直接使用這個函數指針就可以了,固然這項工做是由虛擬機完畢的。
從這裏可以看出。靜態方法就是依據函數名來創建Java函數和JNI函數之間的關聯關係的,它要求JNI層函數的名字必須遵循特定的格式。
這樣的方法也有幾個弊端,它們是:
· 需要編譯所有聲明瞭native函數的Java類。每個生成的class文件都得用javah生成一個頭文件。
· javah生成的JNI層函數名特別長,書寫起來很是不方便。
· 初次調用native函數時要依據函數名字搜索相應的JNI層函數來創建關聯關係,這樣會影響執行效率。
有什麼辦法可以克服上面三種弊端嗎?依據上面的介紹。Java native函數是經過函數指針來和JNI層函數創建關聯關係的。
假設直接讓native函數知道JNI層相應函數的函數指針。不就萬事大吉了嗎?這就是如下要介紹的另一種方法:動態註冊法。
既然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,好像很是麻煩啊!事實上動態註冊的工做。僅僅用兩個函數就能完畢。總結例如如下:
/*
env指向一個JNIEnv結構體。它很是重要,後面會討論它。classname爲相應的Java類名,由於
JNINativeMethod中使用的函數名並非全路徑名。因此要指明是哪一個類。
*/
jclass clazz = (*env)->FindClass(env, className);
//調用JNIEnv的RegisterNatives函數,註冊關聯關係。
(*env)->RegisterNatives(env, clazz, gMethods,numMethods);
因此。在本身的JNI層代碼中使用這樣的方法,就可以完畢動態註冊了。
這裏另外一個很是棘手的問題:這些動態註冊的函數在何時、什麼地方被誰調用呢?好了,不賣關子了。直接給出該問題的答案:
· 當Java層經過System.loadLibrary載入完JNI動態庫後,緊接着會查找該庫中一個叫JNI_OnLoad的函數,假設有,就調用它,而動態註冊的工做就是在這裏完畢的。
因此,假設想使用動態註冊方法,就必需要實現JNI_OnLoad函數,僅僅有在這個函數中,纔有機會完畢動態註冊的工做。靜態註冊則沒有這個要求,可我建議讀者也實現這個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;
}
...... //動態註冊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層也是差異對待這兩者的。先來看基本數據類型的轉換。
基本類型的轉換很是easy。可用表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層數據類型相應的轉換關係。很是easy。只是,應務必注意,轉換成Native類型後相應數據類型的字長。好比jchar在Native語言中是16位,佔兩個字節。這和普通的char佔一個字節的狀況全然不同。
接下來看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)
從上面這段代碼中可以發現:
· 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的靜態函數。
*/
android_media_MediaScanner_processFile(JNIEnv*env,
jobject thiz,
jstring path, jstring mimeType, jobject client)
上面的代碼。引出瞭如下幾節的主角JNIEnv。
JNIEnv是一個和線程相關的,表明JNI環境的結構體。圖2-3展現了JNIEnv的內部結構:
圖2-3 JNIEnv內部結構簡圖
從上圖可知。JNIEnv實際上就是提供了一些JNI系統函數。經過這些函數可以作到:
· 調用Java的函數。
· 操做jobject對象等很是多事情。
後面小節中將詳細介紹怎麼使用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規則中,用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並保存起來以供興許使用。
取出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()
經過本節的介紹。相信讀者已瞭解jfieldID和jmethodID的做用,也知道怎樣經過JNIEnv的函數來操做jobject了。儘管jobject是透明的,但有了JNIEnv的幫助,仍是能輕鬆操做jobject背後的實際對象了。
Java中的String也是引用類型,只是由於它的使用很是頻繁。因此在JNI規範中單首創建了一個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);
......
}
先來看動態註冊中的一段代碼:
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規範定義的函數簽名信息看起來很是彆扭,只是習慣就行了。它的格式是:
(參數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函數。
儘管函數簽名信息很是easy寫錯。但Java提供一個叫javap的工具能幫助生成函數或變量的簽名信息,它的用法例如如下:
javap –s -p xxx。當中xxx爲編譯後的class文件,s表示輸出內部數據類型的簽名信息,p表示打印所有函數和成員的簽名信息,而默認僅僅會打印public成員和函數的簽名信息。
有了javap。就不用死記硬背上面的類型標示了。
咱們知道,Java中建立的對象最後是由垃圾回收器來回收和釋放內存的,可它對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規範已很是好地攻克了這一問題,JNI技術一共提供了三種類型的引用。它們各自是:
· Local Reference:本地引用。在JNI層函數中使用的非全局引用對象都是Local Reference。它包括函數調用時傳入的jobject、在JNI層函數中建立的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建立一個GlobalReference,這樣mClient就不用操心被回收了。
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
......
}
//析構函數
virtual ~MyMediaScannerClient()
{
mEnv->DeleteGlobalRef(mClient);//調用DeleteGlobalRef釋放這個全局引用。
}
每當JNI層想要保存Java層中的某個對象時。就可以使用Global Reference,使用完後記住釋放它就可以了。這一點很是easy理解。如下要講有關LocalReference的一個問題,仍是先看實例。代碼例如如下所看到的:
[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile]
virtualbool scanFile(const char* path, long long lastModified,
long long fileSize)
{
jstringpathStr;
//調用NewStringUTF建立一個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會立刻被回收。這二者看起來沒什麼差異,
只是代碼要是像如下這樣的話,虛擬機的內存就會被很是快被耗盡:
for(inti = 0; i < 100; i++)
{
jstring pathStr = mEnv->NewStringUTF(path);
......//作一些操做
//mEnv->DeleteLocalRef(pathStr); //不立刻釋放Local Reference
}
假設在上面代碼的循環中不調用DeleteLocalRef的話,則會建立100個jstring。
那麼內存的耗費就很是可觀了。
*/
mEnv->DeleteLocalRef(pathStr);
return(!mEnv->ExceptionCheck());
}
因此,沒有及時回收的Local Reference也許是進程佔用過多的一個緣由,請務必注意這一點。
JNI中也有異常,只是它和C++、Java的異常不太同樣。當調用JNIEnv的某些函數出錯後,會產生一個異常,但這個異常不會中斷本地函數的執行,直到從JNI層返回到Java層後,虛擬機纔會拋出這個異常。
儘管在JNI層中產生的異常不會中斷本地函數的執行,但一旦產生異常後,就僅僅能作一些資源清理工做了(好比釋放全局引用。或者ReleaseStringChars)。假設這時調用除上面所說函數以外的其它JNIEnv函數。則會致使程序死掉。
來看一個和異常處理有關的樣例,代碼例如如下所看到的:
[-->android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile函數]
virtualbool scanFile(const char* path, long long lastModified,
long long fileSize)
{
jstring pathStr;
//NewStringUTF調用失敗後,直接返回。不能再幹別的事情了。
if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
......
}
JNI層函數可以在代碼中截獲和改動這些異常,JNIEnv提供了三個函數進行幫助:
· ExceptionOccured函數,用來推斷是否發生異常。
· ExceptionClear函數。用來清理當前JNI層中發生的異常。
· ThrowNew函數。用來向Java層拋出異常。
異常處理是JNI層代碼必須關注的事情。讀者在編寫代碼時務當心對待。
本章經過一個實例介紹了JNI技術中的幾個重要方面。包括:
· JNI函數註冊的方法。
· Java和JNI層數據類型的轉換。
· JNIEnv和jstring的用法,以及JNI中的類型簽名。
· 最後介紹了垃圾回收在JNI層中的使用,以及異常處理方面的知識。
相信掌握了上面的知識後,咱們會對JNI技術有一個比較清晰的認識。
這裏,還要建議讀者再認真閱讀一下JDK文檔中的《Java Native Interface Specification》,它完整和仔細地闡述了JNI技術的各個方面,堪稱深刻學習JNI的權威指南。