一:JNI 概述
JNI全稱爲Java Native Interface,它能夠使在Java虛擬機中運行的Java代碼去操作用其他異於Java的編程語言(如C/C++)所編寫的程序和庫。通俗地說,JNI是一種技術,通過這種技術可以做到如下兩點:
1. Java 程序中的函數可以調用Native 語言寫的函數,Native一般指C/C++編寫的函數。
2. Native程序中的函數可以調用java層的函數,也就是說在C/C++程序中可以調用Java 函數。
在Android 平臺上,JNI就是一座將Native 世界和java世界之間通途的橋,並在大量使用,如surface、mediaPlayer 等等。看圖1-1展示了Android 平臺上JNI所處的位置:
圖1-1 Android 平臺中JNI的示意圖
由上圖可知,JNI將java與Native空間緊密聯繫在一起了。在很多linux版本應用移植到Android 平臺上也可能使用JNI,如一下視頻播放器可以通過JNI無縫過渡到Android平臺上來。
二:JNI 函數註冊
在介紹JNI函數註冊前,先介紹兩個JNI使用中重要的概念:JavaVM與JNIEnv。JavaVM與JNIEnv 本質上都是指向函數表的指針的指針,在C++版本中,它們被定義成類,類裏面包含一個指向函數表的指針。具體參見頭文件定義dalvik\libnativehelper\include\nativehelper\jni.h。JNIEnv是一個與線程相關的變量,不同線程的JNIEnv彼此獨立。JavaVM是虛擬機在JNI層的代表,在一個虛擬機進程中只有一個JavaVM,因此該進程的所有線程都可以使用這個JavaVM。
JNI方法是Android應用程序與本地操作系統直接進行通信的一個手段,這就需要將JAVA與C/C++函數註冊到JNIEnv中,從下面代碼段可以看出註冊過程。
Java 程序:
package com.example.test.app;
public class MiddleWare {
... ...
static {
System.loadLibrary("testmw_jni");
nativeinit();
}
... ...
public native void openUrl(String urlStr);
... ...
public void browserUrl(String url) {
if(StringUtils.isEmpty(url)){
return;
}
openUrl(url);
}
......
}
C++ 程序:
static void _MiddlewareJNI_Native_Init(JNIEnv *env)
{
if (pthread_key_create(&g_EnvKey, _JNI_ThreadDestructor))
return ;
pthread_setspecific(g_EnvKey, env);
jclass clazz;
if (!(clazz = env->FindClass(TEST_CLASS_PATH_NAME)))
return ;
g_InstanceID = env->GetFieldID(clazz, "mNativeContext", "I");
}
static void _MiddlewareJNI_openUrl(JNIEnv *env, jobject thiz, jstring jUrl)
{
MiddlewareJNI* mv = (MiddlewareJNI*)env->GetIntField(thiz, g_InstanceID);
if (mv) {
const char* url = env->GetStringUTFChars(jUrl, 0);
if (url) {
mv->openUrl(url);
env->ReleaseStringUTFChars(jUrl, url);
}
}
}
static JNINativeMethod sMethods[] =
{
{"nativeinit", "()V", (void*)_MiddlewareJNI_Native_Init},
{"openUrl", "(Ljava/lang/String;)V", (void*)_MiddlewareJNI_openUrl}
};
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
jclass clazz = NULL;
JNIEnv* env = NULL;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_4) != JNI_OK)
return JNI_ERR;
if (!(clazz = env->FindClass(TEST_CLASS_PATH_NAME)))
return JNI_ERR;
env->RegisterNatives(clazz, sMethods, sizeof(sMethods) / sizeof(sMethods[0]));
g_JavaVM = vm;
return JNI_VERSION_1_4;
}
涉及到一些項目信息,爲不體現具體項目內容,本文的例子都做過相關修改。以上代碼段是測試實現的例子,Test應用實現成一個testmw_jni 的so文件中。因此,在該JNI方法能夠被調用之前,我們首先要將它加載到當前應用程序進程來,這是通過調用System類的靜態成員函數loadLibrary來實現的。當libtestmw_jni .so文件被加載的時候,函數JNI_OnLoad就會被調用。在函數JNI_OnLoad中,參數vm描述的是當前進程中的Dalvik虛擬機,通過調用它的成員函數GetEnv就可以獲得一個JNIEnv對象。有了這JNIEnv對象之後,我們就可以調用另外一個函數RegisterNatives來向當前進程中的Dalvik虛擬機註冊JNI方法,如上面代碼段描述的nativeinit。註冊過程,將java層的nativeinit() 函數與C/C++ 中的_MiddlewareJNI_Native_Init函數對應起來了。
從上述代碼段中註冊過程可以看出,JAVA層的函數前帶有Java 關鍵字native,它表示對應函數將由JNI層來實現。在JNI層中與之對應的函數都使用static函數。
2.1 JNI數據類型轉換規則
在java調用Native函數傳遞的參數是java數據類型,那麼這些參數類型到了JNI會如何對應需要遵守JNI數據類型轉換規則。Java 數據類型分爲基本數據類型和引用數據類型兩種,JNI層也是區分對待這二者的。詳見表2-1 及表2-2。
表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位 |
表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[] |
jfloatArray |
byte[] |
jbyteArray |
double[] |
jdoubleArray |
java.lang.Trowable實例 |
jthrowable |
|
|
2.2 JNI命名規則與簽名規則
JNI實現方法與Java聲明方法是不同的,例如:Java層聲明的Native方法名是openUrl,而其對應的JNI實現方法的方法名卻是_MiddlewareJNI_openUrl。可見,除了數據類型有對應關係外,方法名也有對應關係。JNI 接口指針是JNI實現方法的第一個參數,其類型是JNIEnv。第二個參數因本地方法是靜態還是非靜態而有所不同。非靜態本地方法的第二個參數是對Java對象的引用,而靜態本地方法的第二個參數是對其 Java 類的引用,其餘的參數都對應於Java 方法的參數。JNI規範裏提供了JNI實現方法的命名規則,方法名由以下幾部分串接而成:
Java_前綴
全限定的類名
下劃線(_)分隔符
增加第一參數JNIEnv* env
增加第二個參數jobject
其他參數按類型映射
返回值按類型映射
再看一段註冊代碼:
static JNINativeMethod sMethods[] =
{
......
{"openUrl", "(Ljava/lang/String;)V", (void*)_MiddlewareJNI_openUrl},
......
}
其中"(Ljava/lang/String;)V"字符是簽名信息,有參數類型和返回值共同組成。之所以要簽名是因爲Java支持函數重載,可以定義相同的函數,不同的參數。僅僅通過函數名沒有辦法找到具體函數,爲了解決這一問題,JNI技術將參數類型和返回值類型組合成一個函數的簽名信息,有了簽名信息和函數名,很順利能找到java中的函數了。
JNI規範定義的簽名格式如下:
(參數1類型標示參數2類型標示... 參數n類型標示)返回值類型標示
常見的類型標示示意如表2-3,java還存在其他簽名的方式,如函數簽名,這裏不一一舉例。
表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 |
|
|
2.3. 正向和反向通信
JNI是連通java層與C/C++層的橋樑,存在雙向通信,正向java層調用C/C++ 層,反向C/C++調用java層。 Java 調用native 方法比較簡單,framework中也有不少參考代碼。 上述代碼段也給出了示例,Java層調用native方法openUrl,JVM將會調用到native的_MiddlewareJNI_openUrl方法,其中在native method中,JNIEnv作爲第一個參數傳入,通過JNIEnv獲取相應對象來處理相關實現。
反向通信是很容易使用錯誤,JNI開發最常見的錯誤就是濫用了JNIEnv接口。需要強調的是JNIEnv是跟線程相關的,而JavaVM是進程相關。要用的時候在不同線程中再通過JavaVM *jvm的方法來獲取與當前線程相關的JNIEnv*。
正如前面所述,不論進程中有多少線程,JavaVM進程只有一份,JNIEnv與JavaVM關係如下:
1.調用JavaVM的AttachCurrentThread函數,就可以得到這個線程的JNIEnv結構體,這樣就可以在後臺線程中回調java函數。
2.在後臺線程退出前,需要調用JavaVM 的DetachCurrentThread 函數來釋放對應的資源。