Android JNI 介紹

一: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的示意圖

    

由上圖可知,JNIjavaNative空間緊密聯繫在一起了。在很多linux版本應用移植到Android 平臺上也可能使用JNI,如一下視頻播放器可以通過JNI無縫過渡到Android平臺上來。


二:JNI 函數註冊

         在介紹JNI函數註冊前,先介紹兩個JNI使用中重要的概念:JavaVMJNIEnvJavaVMJNIEnv 本質上都是指向函數表的指針的指針,在C++版本中,它們被定義成類,類裏面包含一個指向函數表的指針。具體參見頭文件定義dalvik\libnativehelper\include\nativehelper\jni.h。JNIEnv是一個與線程相關的變量,不同線程的JNIEnv彼此獨立JavaVM是虛擬機在JNI層的代表,在一個虛擬機進程中只有一個JavaVM,因此該進程的所有線程都可以使用這個JavaVM

        JNI方法是Android應用程序與本地操作系統直接進行通信的一個手段,這就需要將JAVAC/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-3java還存在其他簽名的方式,如函數簽名,這裏不一一舉例。

                    表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 函數來釋放對應的資源。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章