歡迎光臨
每天分享高質量文章

Java 中 JNI 的使用 ( 上 )

(點選上方公眾號,可快速關註)


來源:Young_Blog ,

landerlyoung.github.io/blog/2014/10/16/java-zhong-jnide-shi-yong/

JNI 全稱是 Java Native Interface。是在 Java 和 Native 層(包括但不限於C/C++)相互呼叫的介面規範。

JNI 在 Java 1.1中正式推出,在 Java 1.2版本中加入了 JNI_OnLoad、JNI_OnUnload 方法,這兩個方法還是很有用的,後面再說。

JNI基礎篇

Java 透過 JNI 呼叫本地方法的過程大致是:

  1. 寫一個 Java 類,在其中宣告對應要呼叫的 native 方法,用 native 關鍵字修飾。 比如 private static native int native_newInstance();

  2. 透過 javah 命令生成 Java 類對應的 C/C++ 頭檔案。javah -encoding utf-8 -cp src com.young.soundtouch.SoundTouch;

  3. 在 C/C++ 中實現頭檔案中宣告的函式;

  4. 編譯 C/C++ 程式碼為動態庫(Windows中的dll、Linux/Android 中的 so、MAC OSX 中的 dylib);

  5. 在 Java 程式碼中載入動態庫,即可像呼叫 Java 方法一樣,呼叫到 native 函式。

其中第3步在 Java 1.2 中增加了 JNI_OnLoad 方法之後有另一種實現方式(後面說)。

javah 生成的頭檔案大致是這樣的:

/* DO NOT EDIT THIS FILE – it is machine generated */

#include

/* Header for class com_young_soundtouch_SoundTouch */

 

#ifndef _Included_com_young_soundtouch_SoundTouch

#define _Included_com_young_soundtouch_SoundTouch

#ifdef __cplusplus

extern “C” {

#endif

#undef com_young_soundtouch_SoundTouch_SETTING_USE_AA_FILTER

#define com_young_soundtouch_SoundTouch_SETTING_USE_AA_FILTER 0L

    /*

    * Class:     com_young_soundtouch_SoundTouch

    * Method:    native_getDefaultSampleElementSize

    * Signature: ()I

    */

    JNIEXPORT jint JNICALL Java_com_young_soundtouch_SoundTouch_native_1getDefaultSampleElementSize

        (JNIEnv *, jclass);

#ifdef __cplusplus

}

#endif

#endif

檔案開頭就是普通的頭檔案,但是可以發現:

1. 包含了 jni.h 頭檔案(一般位於 $JAVA_HOME/jd{jdk-version}/include 文目錄內)。這是 JNI 中所有的型別、函式、宏等定義的地方。所以C/C++世界的JNI是由他制定的遊戲規則。

2. 在類中生命的常量(static final)型別會在頭檔案中以宏的形式出現,這一點還是很方便的。

3. 函式的註釋還是比較全的,包括了:

  • 對應的 class

  • 對應的 Java 方法名

  • 對應 Java 方法的簽名

4. 方法的宣告顯得有點奇怪,由以下及部分組成:

  • JNIEXPORT這是函式的匯出方式;

  • jint 傳回值型別(jint 由 jni.h 定義,對應 int,下麵具體再說吧);

  • JNICALL 函式的呼叫方式也就是彙編級別引數的傳入方式;

  • Java_com_young_soundtouch_SoundTouch_native_1getDefaultSampleElementSize —— 超級長的函式名!!!格式是 Java_ + 類全名 + _ + JAVA 中宣告的native方法名。其中會把包名中的點(.)替換成下劃線(_),同時為了避免衝突把下劃線替換成_1;

  • 方法的引數,上面的這個方法在 Java 的宣告中實際上是沒有引數的,其中的 JNIENV 顧名思義是 JNI 環境,和具體的執行緒系結。而第二個引數 jclass 其實是 Java 中的 Class。因為上面是一個 static 方法,因此第二個引數是 jclass。如果是一個實體方法則對應第二個引數是 jobject,相當於 Java中的 this。

下麵在 C/C++ 中實現這個方法就行啦。但是在動手前現大致瞭解以下 jni.h 制定的遊戲規則。

型別轉換

javah 生成的頭檔案裡面使用的型別都是 jni.h 定義的,目的是做到平臺無關,比如保證在所有平臺上 jint 都是32位的有符號整型。

基本對應關係如下:

取用型別對應關係:

透過表格發現,除了上面定義的 String、Class、Throwable,其他的類(除了陣列)都是以 jobject 的形式出現的!事實上 jstring、 jclass 也都是 object 的子類。所以這裡還是和 Java 層一樣,一切皆 jobject。(當然,如果 jni 在 C 語言中編譯的話是沒有繼承的概念的,此時 jstring、jclass 等其實就是 jobject!用了 typedef 轉換而已!!)

接下來是 JNIEnv * 這個指標,它提供了 JNI 中的一系列操作的介面函式。

JNI 中操作 jobject

其實也就是在native層操作 Java 層的實體。 要操作一個實體無疑是:

  1. 獲取/設定 (即 get/set )成員變數(field)的值;

  2. 呼叫成員方法(method)。

所以問題來了:(挖掘機技術哪家強?! o(*≧▽≦)ツ┏━┓ )

怎麼得到 field 和 method?

透過使用 jfieldID 和 jmethodID: 在 JNI 中使用類似於放射的方式來進行 field 和 method 的操作。JNI 中使用j fieldID 和 jmethodID 來表示成員變數和成員方法,獲取方式是:

jfieldID GetFieldID(jclass clazz, const char *name, const char *sig);

jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig);

jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);

jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig) ;

其中最後一個引數是簽名。 獲取jclass的方法除了實用上面靜態方法的第二個引數外,還可以手動獲取。 jclass FindClass(const char *name) 需要註意的是name引數,他是一個類包括包名的全稱,但是需要把包名中的點.替換成斜槓/。(好吧,事實上我不是太明白為啥要這麼做。)

有了 jfieldID 和 jmethodID 就知道狗蛋住哪了,現在去狗蛋家找他玩 ♪(^∇^*)

1. get:

  • GetField(jobject , jfieldID); 即可獲得對應的field,其中field的型別是type,可以是上面型別所敘述的任何一種

  • GetStaticField(jobject , jfieldID); 同1,唯一的區別是用來獲取靜態成員。

2. set:

  • void SetField(jobject obj, jfieldID fieldID, val)

  • void SetStaticField(jclass clazz, jfieldID fieldID, value);

成員方法:

呼叫方法自然要把方法的引數傳遞進去,JNI中實現了三種引數的傳遞方式:

  1. CallMethod(jobject obj, jmethod jmethodID, …) 其中 … 是 C 中的可變長引數,類似於 printf 那樣,可以傳遞不定長個引數。於是你可以把 Java 方法需要的引數在這裡面傳遞進去。

  2. CallMethodV(jobject obj, jmethodID methodID, va_list args) 其中的 va_list 也是 C 中可變長引數相關的內容(我不瞭解,不敢瞎說,偷懶粘一下Oracle的檔案)“Programmers place all arguments to the method in an args argument of type va_list that immediately follows the methodID argument. The CallMethodV routine accepts the arguments, and, in turn, passes them to the Java method that the programmer wishes to invoke.”

  3. CallMethodA(jobject obj, jmethodID methodID, const jvalue * args) 哎!這個我知道可以說兩句 LOL ~~這裡的 jvalue 透過查程式碼發現就是 JNI 中各個資料型別的 union,所以可以使用任何型別複製!所以引數的傳入方式是透過一個 jvalue 的陣列,陣列內的元素可以是任何 jni 型別。

然後問題又來了:(挖掘機技術到底哪家強?!o(*≧▽≦)ツ┏━┓) 如果傳進來的引數和java宣告的引數的不一致會怎麼樣!(即不符合方法簽名)這裡檔案中沒用明確解釋,但是說道: > Exceptions raised during the execution of the Java method.

typedef union jvalue {

    jboolean z;

    jbyte    b;

    jchar    c;

    jshort   s;

    jint     i;

    jlong    j;

    jfloat   f;

    jdouble  d;

    jobject  l;

} jvalue;

1. 呼叫實體方法(instance method):

  • CallMethod(jobject obj, jmethodID methodID, …); 呼叫一個具有型別傳回值的方法。

  • CallMethodV(jobject obj, jmethodID methodID, va_list args);

  • CallMethodA(jobject obj, jmethodID methodID, const jvalue * args)

2. 呼叫靜態方法(static method):

  • CallStaticMethod(jobject obj, jmethodID methodID, …);

  • CallStaticMethodV(jobject obj, jmethodID methodID, va_list args);

  • CallStaticMethodA(jobject obj, jmethodID methodID, const jvalue * args)

3. 呼叫父類方法(super.method),這個就有點不一樣了。多了一個 jclass 引數,jclass 可以使 obj 的父類,也可以是 obj 自己的class,但是 methodID 必須是從 jclass 獲取到的,這樣就可以呼叫到父類的方法。

  • CallNonvirtualMethod(jobject obj, jclass clazz, jmethodID methodID, …)

  • CallNonvirtualMethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args);

  • CallNonvirtualMethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args);

【關於投稿】


如果大家有原創好文投稿,請直接給公號傳送留言。


① 留言格式:
【投稿】+《 文章標題》+ 文章連結

② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~



看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂