[深入理解Android卷一 全文-第二章]深入理解JNI
由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容。
第2章 深入理解JNI
本章主要內容
· 通過一個例項,介紹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
2.1 概述
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平臺中多媒體系統的重要組成部分,它的功能是掃描媒體檔案,得到諸如歌曲時長、歌曲作者等媒體資訊,並將它們存入到媒體資料庫中,供其他應用程式使用。
2.2.1 Java層的MediaScanner分析
來看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的關鍵字,表示它將由JNI層完成。
privatestatic native final void native_init();
......
privatenative void processFile(String path, String mimeType,
MediaScannerClient client);
......
}
· 上面程式碼中列出了兩個比較重要的要點:
1. 載入JNI庫
前面說過,如Java要呼叫Native函式,就必須通過一個位於JNI層的動態庫才能做到。顧名思義,動態庫就是執行時載入的庫,那麼是什麼時候,在什麼地方載入這個庫呢?
這個問題沒有標準答案,原則上是在呼叫native函式前,任何時候、任何地方載入都可以。通行的做法是,在類的static語句中載入,通過呼叫System.loadLibrary方法就可以了。這一點,在上面的程式碼中也見到了,我們以後就按這種方法編寫程式碼即可。另外,System.loadLibrary函式的引數是動態庫的名字,即media_jni。系統會自動根據不同的平臺拓展成真實的動態庫檔名,例如在Linux系統上會拓展成libmedia_jni.so,而在Windows平臺上則會拓展成media_jni.dll。
解決了JNI庫載入的問題,再來來看第二個關鍵點。
2. Java的native函式和總結
從上面程式碼中可以發現,native_init和processFile函式前都有Java的關鍵字native,它表示這兩個函式將由JNI層來實現。
Java層的分析到此結束。JNI技術也很照顧Java程式設計師,只要完成下面兩項工作就可以使用JNI了,它們是:
· 載入對應的JNI庫。
· 宣告由關鍵字native修飾的函式。
所以對於Java程式設計師來說,使用JNI技術真的是太容易了。不過JNI層可沒這麼輕鬆,下面來看MS的JNI層分析。
2.2.2 JNI層的MediaScanner分析
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函式呢?下面就來回答這個問題。
1. 註冊JNI函式
正如程式碼中註釋的那樣,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函式的註冊實際上有兩種方法,下面分別做介紹。
(1)靜態方法
我們從網上找到的與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函式的。其實,過程非常簡單:
· 當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層對應函式的函式指標,不就萬事大吉了嗎?這就是下面要介紹的第二種方法:動態註冊法。
(2)動態註冊
既然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即可。
2. 資料型別轉換
通過前面的分析,解決了JNI函式的註冊問題。下面來研究資料型別轉換的問題。
在Java中呼叫native函式傳遞的引數是Java資料型別,那麼這些引數型別到了JNI層會變成什麼呢?
Java資料型別分為基本資料型別和引用資料型別兩種,JNI層也是區別對待這二者的。先來看基本資料型別的轉換。
(1)基本型別的轉換
基本型別的轉換很簡單,可用表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位,佔兩個位元組,這和普通的char佔一個位元組的情況完全不一樣。
接下來看Java引用資料型別的轉換。
(2)引用資料型別的轉換
引用資料型別的轉換如表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。
3. 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的作用。
4. 通過JNIEnv操作jobject
前面提到過一個問題,即Java的引用型別除了少數幾個外,最終在JNI層都用jobject來表示物件的資料型別,那麼該如何操作這個jobject呢?
從另外一個角度來解釋這個問題。一個Java物件是由什麼組成的?當然是它的成員變數和成員函數了。那麼,操作jobject的本質就應當是操作這些物件的成員變數和成員函式。所以應先來看與成員變數及成員函式有關的內容。
(1)jfieldID 和jmethodID的介紹
我們知道,成員變數和成員函式是由類定義的,它是類的屬性,所以在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後,又該怎麼用它呢?
(2)使用jfieldID和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背後的實際物件了。
5. jstring介紹
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);
......
}
6. JNI型別簽名的介紹
先來看動態註冊中的一段程式碼:
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函式。
雖然函式簽名信息很容易寫錯,但Java提供一個叫javap的工具能幫助生成函式或變數的簽名信息,它的用法如下:
javap –s -p xxx。其中xxx為編譯後的class檔案,s表示輸出內部資料型別的簽名信息,p表示列印所有函式和成員的簽名信息,而預設只會列印public成員和函式的簽名信息。
有了javap,就不用死記硬背上面的型別標示了。
7. 垃圾回收
我們知道,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,使用完後記住釋放它就可以了。這一點很容易理解。下面要講有關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或許是程序佔用過多的一個原因,請務必注意這一點。
8. JNI中的異常處理
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層程式碼必須關注的事情,讀者在編寫程式碼時務小心對待。
2.3 本章小結
本章通過一個例項介紹了JNI技術中的幾個重要方面,包括:
· JNI函式註冊的方法。
· Java和JNI層資料型別的轉換。
· JNIEnv和jstring的使用方法,以及JNI中的型別簽名。
· 最後介紹了垃圾回收在JNI層中的使用,以及異常處理方面的知識。
相信掌握了上面的知識後,我們會對JNI技術有一個比較清晰的認識。這裡,還要建議讀者再認真閱讀一下JDK文件中的《Java Native Interface Specification》,它完整和細緻地闡述了JNI技術的各個方面,堪稱深入學習JNI的權威指南。
相關推薦
[深入理解Android卷一 全文-第二章]深入理解JNI
由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容。第2章 深入理解JNI本章主要內容· 通過一個例項,介紹JNI技術和在使用中應注意的問題。本章涉
[深入理解Android卷一全文-第六章]深入理解Binder
由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容。第6章 深入理解Binder本章主要內容· 以MediaServer為切入點,對Binder的工作
[深入理解Android卷一全文-第三章]深入理解init
由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容。第3章 深入理解init本章主要內容· 深入分析init。本章涉及的原始碼檔名及位置下面是本章分
深入理解Android卷III 第7章 深入理解SystemUI (節選)
多謝華章圖書與鄧凡平先生的幫助,《深入理解Android卷III〉終於上市了。歡迎大家來這裡一起探討文中的問題或與Android系統有關的任何話題。 第7章深入理解SystemUI 本章主要內容: 探討狀態列與導航欄的啟動過程
[深入理解Android卷二 全文-第三章]深入理解SystemServer
由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容 第3章 深入理解SystemServer本章主要內容:· 分析SystemServer· 分析
[深入理解Android卷二 全文-第五章]深入理解PowerManagerService
由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容第5章 深入理解PowerManagerService本章主要內容:· 深入分析PowerMana
深入理解計算機系統筆記之第二章(一)
資訊的表示和處理(一) 大多數計算機使用8位的塊(也就是一個位元組byte),由此可以看到32位(4個位元組)系統和64位(8個位元組)系統的區別。32位系統在於cpu可以同時處理4個位元組(32位)的資料,那麼64位系統cpu可以同時處理8個位元組(64位)的資料。 一個
《深入理解Android 卷III》第八章深入理解Android桌布(節選)
第8章 深入理解Android桌布(節選) 本章主要內容: · 討論動態桌布的實現。 · 在動態桌布的基礎上討論靜態桌布的實現。 · 討論WMS對桌布視窗所做的特殊處理。 本章涉及的原始碼檔名及位置: · Wal
《深入理解Android 卷III》第四章 深入理解WindowManagerService
《深入理解Android 卷III》即將釋出,作者是張大偉。此書填補了深入理解Android Framework卷中的一個主要空白,即Android Framework中和UI相關的部分。在一個特別講究顏值的時代,本書分析了Android 4.2中WindowManagerS
《深入理解Android 卷III》第五章 深入理解Android輸入系統
《深入理解Android 卷III》即將釋出,作者是張大偉。此書填補了深入理解Android Framework卷中的一個主要空白,即Android Framework中和UI相關的部分。在一個特別講究顏值的時代,本書分析了Android 4.2中WindowManagerS
[讀書筆記][第二章] 深入理解C# -- C# in depth
ch2 C#1所搭建的核心基礎 委託 宣告委託 方法執行程式碼:相容的方法簽名 建立委託例項 呼叫例項:Invoke() 或簡化呼叫 加減委託 呼叫列表,Combine() + , Remove() - 事件 事件是委託型別,是屬性,封裝了publish
《深入理解Android 卷III》推薦序
轉載:https://blog.csdn.net/innost/article/details/47292791《深入理解Android 卷III》即將釋出,作者是張大偉。此書填補了深入理解Android Framework卷中的一個主要空白,即Android Framewo
JNI(深入理解Android卷I)的讀書筆記
一:概述 JNI:Java Native Interface。 作用:連線Java世界和Native世界。Java程式中函式可以呼叫Native語言寫的函式;Native程式中的函式可以呼叫Java層的函式。 二:例項:MediaScanner 2.1 關係: Java層(
深入理解android(卷1)pdf
下載地址:網盤下載一本以情景方式對Android的原始碼進行深入分析的書。內容廣泛,以對Framework層的分析為主,兼顧Native層和Application層;分析深入,每一部分原始碼的分析都力求透徹;針對性強,注重實際應用開發需求,書中所涵蓋的知識點都是Android
深入理解 Android 卷I
原文地址:http://wiki.jikexueyuan.com/project/deep-android-v1/ 第8章 深入理解Surface系統 本章主要內容 · 詳細分析一個Activity的顯示過程。 · 詳細分析Surface
深入理解Android(一):Gradle詳解
作者 鄧凡平 編者按:隨著移動裝置硬體能力的提升,Android系統開放的特質開始顯現,各種開發的奇技淫巧、黑科技不斷湧現,InfoQ特聯合《深入理解Android》系列圖書作者鄧凡平,開設深入理解Android專欄,探索Android從框架到應用開
《深入理解java虛擬機器》第二章筆記
1. 執行時資料區域 名稱 是否共享 作用 存在的異常 程式計數器 執行緒私有 如果執行的是java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址 java虛擬機器棧 執行緒私有 每個
深入理解Android之Java Security第二部分(Final)
深入理解Android之Java Security(第二部分,最後)程式碼路徑:Security.java:libcore/lunl/src/main/java/java/security/TrustedCertificateStore.java:libcore /crypt
《第一行程式碼Android》學習總結第二章 Activity建立與相關設定
一、id標籤 如果在XML檔案中引用一個id,則使用@id/id_name; 如果在XML檔案中定義一個id,則使用@+id/id_name。 二、程式中設定主活動 在AndroidMaifest.xml中設定 <intent-filter>
第二章:理解DispatcherServlet ——深入淺出學Spring Web MVC
整合Web環境的通用配置: <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:spring-