JNI概述
- Java程序中的函數(shù)可以調(diào)用Native語言寫的函數(shù),Native一般指的是C/C++寫的函數(shù)
- Native層中的函數(shù)可以調(diào)用Java層的函數(shù),也就是在C/C++程序中調(diào)用Java的函數(shù)
要想做到以上兩點,JNI(Java Native Interface)技術(shù)應(yīng)運(yùn)而生。
深入分析后可以知道,雖然Java代碼具有平臺無關(guān)性,但運(yùn)行在具體平臺上的Java虛擬機(jī)并不能做到這一點,但是JNI技術(shù)的出現(xiàn)屏蔽了不同平臺的差異,使得上層的Java具有了平臺無關(guān)的特性。

MediaScanner的分析
android大量使用了JNI技術(shù),書中分析了MediaScanner這個例子。
- Java世界對應(yīng)的是MediaScanner類,這個類一些函數(shù)需要Native層來實現(xiàn),比如對目錄的掃描。
- Native層對應(yīng)是libmedia.so,他完成了實際的功能。
- JNI層對應(yīng)的是libmedia_jni.so,這里可以看出JNI層其實也是Native代碼寫的。其中下劃線前的media是Native層庫的名字,這里指的就死libmedia庫,android對JNI庫的名字有規(guī)范,一般是:
lib模塊名_jni.so - Java層的MediaScanner將通過libmedia_jni.so來與Native層的libmedia.so交互。
Java層的MediaScanner
framewor/base/media/java/android/media/MediaScanner.java是MediaScanner在Java層源碼的位置。
public class MediaScanner
{
static {
/**加載對應(yīng)的JNI庫,media_jni是對應(yīng)JNI庫的名字,實際上加載動態(tài)庫的時候他會拓展成libmedia_jni.so **/
System.loadLibrary("media_jni");
native_init(); //調(diào)用native_init()函數(shù),他是一個native函數(shù)
}
......
//掃面目錄的函數(shù),里面用到了processDirectory這個native函數(shù)
public void scanDirectories(String[] directories, String volumeName) {
......
for (int i = 0; i < directories.length; i++) {
processDirectory(directories[i], mClient);
}
......
private native void processDirectory(String path, MediaScannerClient client);
private static native final void native_init();
......
這里說明了兩點內(nèi)容,在需要native函數(shù)的java類中會包括JNI的加載和Native函數(shù)的聲明,指示它由JNI層去完成。
- JNI加載流程
Java層要調(diào)用Native函數(shù),必須通過位于JNI層的動態(tài)庫才能實現(xiàn)。動態(tài)庫的必須在運(yùn)行時加載,所以,我們通常的做法就是在類的static語句中加入System.loadLibrary方法完成對動態(tài)庫的加載,系統(tǒng)會自動將參數(shù)轉(zhuǎn)化成對應(yīng)環(huán)境下真實動態(tài)庫的名字。linux上是so文件,win下就是dll文件。 - Java的native函數(shù)
加載完JNI庫后,我們只需要在Java中聲明由關(guān)鍵字native修飾的函數(shù)即可。是不是很簡單,但是JNI層的MediaScanner就沒那么輕松了。
JNI層的MediaScanner
JNI層的MediaScanner代碼位于
frameworks/base/media/jni/android_media_MediaScanner.cpp
上面再Java層聲明的native_init和processFile在這里都有具體的實現(xiàn),那么問題就來了,Java層聲明的函數(shù)如何才能對應(yīng)到JNI層的。
- 注冊JNI函數(shù)

native_init函數(shù)位于android.media這個包中,這樣可以得到它的全路徑名稱android.media.MediaScanner.native_init,之后再將.全部換成_,native_init就找到了JNI層的實現(xiàn)。
上述就是JNI函數(shù)注冊的問題
-
靜態(tài)注冊
流程如下:
注意:因為packagename是該類完整的包名,所以javah命令需要在包外面執(zhí)行,否則會找不到該類
靜態(tài)注冊的弊端:1、需要編譯所有聲明了native函數(shù)的java類。2、javah生成的jni函數(shù)名會特別長。 3、初次調(diào)用native函數(shù)時要根據(jù)函數(shù)名來搜索對應(yīng)JNI層的函數(shù),并建立關(guān)聯(lián),這會影響效率。
-
動態(tài)注冊
既然Java Native函數(shù)和JNI函數(shù)一一對應(yīng),能不能有這種結(jié)構(gòu)可以保存這種關(guān)聯(lián)關(guān)系呢,肯定是有的,在JNI技術(shù)中有一種JNINativeMethod的結(jié)構(gòu),它記錄下了這些關(guān)聯(lián)關(guān)系。其實Android大部分采用的是動態(tài)注冊的方法
MediaScanner的JNI層中的示例:
static const JNINativeMethod gMethods[] = {
{
"processDirectory",
"(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void )android_media_MediaScanner_processDirectory
},
{
"processFile",
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void )android_media_MediaScanner_processFile
},
.......
{
"native_init",
"()V",
(void )android_media_MediaScanner_native_init
},
......
};
基本結(jié)構(gòu)定義:
typedef struct {
//java類中native函數(shù)的名字,不用攜帶包名,例如“native_init”
const char name;
//java函數(shù)的簽名信息,用字符串表示,是參數(shù)類型和返回值類型的組合
const char signature;
//JNI層對應(yīng)函數(shù)的函數(shù)指針,它是void 類型
void* fnPtr;
}JNINativeMethod;定義完畢之后,需要執(zhí)行注冊
int register_android_media_MediaScanner(JNIEnv *env){ return AndroidRuntime::registerNativeMethods(env, kClassMediaScanner, gMethods, NELEM(gMethods)); }我們發(fā)現(xiàn)AndroidRunTime類提供了注冊函數(shù),而它又調(diào)用了JNIHelp中的jniRegisterNativeMethods方法,最終會走到JNIEnv的RegisterNatives完成注冊工作。
那到底是什么時候去執(zhí)行這個?
在Java層執(zhí)行System.loadLibrary加載JNI動態(tài)庫后,緊接著會查找該庫中一個JNI_OnLoad的函數(shù),如果有的話,動態(tài)注冊就在這里完成。然而在MediaScanner對應(yīng)的android_media_MediaScanner.cpp中并沒有發(fā)現(xiàn)這個函數(shù)。由于多媒體系統(tǒng)中很對地方用到JNI,所以register_android_media_MediaScanner這個注冊方法被放在了android_media_MediaPlayer.cpp的JNI_OnLoad方法中,當(dāng)然還有其它的多媒體相關(guān)的注冊函數(shù)。
數(shù)據(jù)類型轉(zhuǎn)換
主要解決的問題是:在Java中調(diào)用native函數(shù)傳遞的參數(shù)是Java數(shù)據(jù)類型,如何將他們轉(zhuǎn)化成Native的數(shù)據(jù)類型。
Java分基本數(shù)據(jù)類型和引用數(shù)據(jù)類型,先看基本數(shù)據(jù)類型:
| 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位 |
這里要注意的是轉(zhuǎn)換成Native類型后對應(yīng)的數(shù)據(jù)類型的字長,char在Java中是占兩個字節(jié),jchar同樣也是,要區(qū)別于普通char只占一個字節(jié)。
引用類型的轉(zhuǎn)換
| Java引用類型 | Native類型 | Java引用類型 | Native類型 | |
|---|---|---|---|---|
| All object | jobject | char[] | jcharArray | |
| java.lang.Class實例 | jclass | short[] | jshortArray | |
| java.lang.String實例 | jstring | int[] | jintArray | |
| Object[] | jobjectArray | long[] | jlongArray | |
| boolean[] | jbooleanArray | flaot[] | jflaotArray | |
| byte[] | jbyteArray | double[] | jdoubleArray | |
| java.lang.Throwable實例 | jthrowable |
再拿MediaScanner舉例,其中的processFile
android_media_MediaScanner_processFile( JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client){
Java層的函數(shù)中只傳了三個參數(shù),即path,mineType和client,這里它們的類型都轉(zhuǎn)化成了native類型。前兩個參數(shù)分別是JNIEnv和Java層的MediaScanner對象(jobject)
JNIEnv
JNIEnv其實是一個與線程相關(guān)的代表JNI環(huán)境的結(jié)構(gòu)體。

JNIEnv提供了JNI相關(guān)的系統(tǒng)函數(shù),這些函數(shù)可以做到:調(diào)用Java函數(shù)和操作jobject對象等。
- 通過JNIEnv操作jobject
在JNI規(guī)則中,用jfiledID和jMethodID來表示java類的成員變量和成員函數(shù)。比如MyMediaScannerClient中。
代碼中將scanFile和handleStringTag函數(shù)的jmethodID保存為MyMediaScannerClient的成員變量,這是為了解決每次操作object都會去查詢jmethodID或jfieldID而帶來的運(yùn)行效率降低。
使用可以看virtual status_t scanFile中的mEnv->CallVoidMethod
完成JNI層調(diào)用Java對象的函數(shù)。 - jstring
JNIEnv提供有關(guān)jstring的函數(shù) - JNI簽名介紹
Java函數(shù)支持函數(shù)重載,也就是說,可以定義同名但是不同參數(shù)的函數(shù),但僅僅根據(jù)函數(shù)名是沒法找到具體函數(shù)的。為了解決這個問題,JNI技術(shù)就將參數(shù)類型和返回值類型的組合作為一個函數(shù)的簽名信息,有了這個簽名和函數(shù)名,就能很順利的找到Java中的函數(shù)。 - 垃圾回收
JNI的三種類型引用技術(shù):
local reference:本地引用,一旦JNI函數(shù)返回時,jobject就可能會被回收
Global reference:全局引用,不主動釋放不會被回收
Weak Global reference:特殊的全局引用,可能會被回收,每次使用時需要判斷是否回收 -
JNI中的異常處理
JNIEnv提供了三個函數(shù)給予幫助
ExceptionOccured函數(shù),用來判斷是否發(fā)生異常
ExceptionClear函數(shù),用來清理JNI層發(fā)生的異常。
ThrownNew函數(shù),用來Java層拋出異常。

