(點選上方公眾號,可快速關註)
來源: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 呼叫本地方法的過程大致是:
-
寫一個 Java 類,在其中宣告對應要呼叫的 native 方法,用 native 關鍵字修飾。 比如 private static native int native_newInstance();
-
透過 javah 命令生成 Java 類對應的 C/C++ 頭檔案。javah -encoding utf-8 -cp src com.young.soundtouch.SoundTouch;
-
在 C/C++ 中實現頭檔案中宣告的函式;
-
編譯 C/C++ 程式碼為動態庫(Windows中的dll、Linux/Android 中的 so、MAC OSX 中的 dylib);
-
在 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 層的實體。 要操作一個實體無疑是:
-
獲取/設定 (即 get/set )成員變數(field)的值;
-
呼叫成員方法(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:
-
Get Field(jobject , jfieldID); 即可獲得對應的field,其中field的型別是type,可以是上面型別所敘述的任何一種 -
GetStatic Field(jobject , jfieldID); 同1,唯一的區別是用來獲取靜態成員。
2. set:
-
void Set
Field(jobject obj, jfieldID fieldID, val) -
void SetStatic
Field(jclass clazz, jfieldID fieldID, value);
成員方法:
呼叫方法自然要把方法的引數傳遞進去,JNI中實現了三種引數的傳遞方式:
-
Call
Method(jobject obj, jmethod jmethodID, …) 其中 … 是 C 中的可變長引數,類似於 printf 那樣,可以傳遞不定長個引數。於是你可以把 Java 方法需要的引數在這裡面傳遞進去。 -
Call
MethodV(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.” -
Call
MethodA(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):
-
Call Method(jobject obj, jmethodID methodID, …); 呼叫一個具有 型別傳回值的方法。 -
Call MethodV(jobject obj, jmethodID methodID, va_list args); -
Call
MethodA(jobject obj, jmethodID methodID, const jvalue * args)
2. 呼叫靜態方法(static method):
-
CallStatic Method(jobject obj, jmethodID methodID, …); -
CallStatic MethodV(jobject obj, jmethodID methodID, va_list args); -
CallStatic
MethodA(jobject obj, jmethodID methodID, const jvalue * args)
3. 呼叫父類方法(super.method),這個就有點不一樣了。多了一個 jclass 引數,jclass 可以使 obj 的父類,也可以是 obj 自己的class,但是 methodID 必須是從 jclass 獲取到的,這樣就可以呼叫到父類的方法。
-
CallNonvirtual Method(jobject obj, jclass clazz, jmethodID methodID, …) -
CallNonvirtual MethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); -
CallNonvirtual MethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args);
【關於投稿】
如果大家有原創好文投稿,請直接給公號傳送留言。
① 留言格式:
【投稿】+《 文章標題》+ 文章連結
② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/
③ 最後請附上您的個人簡介哈~
看完本文有收穫?請轉發分享給更多人
關註「ImportNew」,提升Java技能