Java中JNI的使用(上)
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 <jni.h> /* 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
檔案開頭就是普通的標頭檔案,但是可以發現:
- 包含了 jni.h 標頭檔案(一般位於 $JAVA_HOME/jd{jdk-version}/include 文目錄內)。這是 JNI 中所有的型別、函式、巨集等定義的地方。所以C/C++世界的JNI是由他制定的遊戲規則。
- 在類中生命的常量(static final)型別會在標頭檔案中以巨集的形式出現,這一點還是很方便的。
- 函式的註釋還是比較全的,包括了:
- 對應的 class
- 對應的 Java 方法名
- 對應 Java 方法的簽名
- 方法的宣告顯得有點奇怪,由以下及部分組成:
- 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) ;
其中最後一個引數是 ofollow,noindex" target="_blank">簽名 。 獲取jclass的方法 除了實用上面靜態方法的第二個引數外,還可以手動獲取。 jclass FindClass(const char *name)
需要注意的是 name
引數,他是一個類包括包名的全稱,但是需要把包名中的點 .
替換成斜槓 /
。(好吧,事實上我不是太明白為啥要這麼做。)
有了 jfieldID 和 jmethodID 就知道狗蛋住哪了,現在去狗蛋家找他玩 ♪(^∇^*)
1. get:
-
<type> Get<type>Field(jobject , jfieldID);
即可獲得對應的field,其中field的型別是type,可以是上面 型別 所敘述的任何一種 -
<type> GetStatic<type>Field(jobject , jfieldID);
同1,唯一的區別是用來獲取靜態成員。
2. set:
- void Set<type>Field(jobject obj, jfieldID fieldID, <type> val)
- void SetStatic<type>Field(jclass clazz, jfieldID fieldID, <type> value);
成員方法:
呼叫方法自然要把方法的引數傳遞進去,JNI中實現了三種引數的傳遞方式:
-
Call<type>Method(jobject obj, jmethod jmethodID, ...)
其中...
是 C 中的可變長引數,類似於printf
那樣,可以傳遞不定長個引數。於是你可以把 Java 方法需要的引數在這裡面傳遞進去。 -
Call<type>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<type>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; jbyteb; jcharc; jshorts; jinti; jlongj; jfloatf; jdoubled; jobjectl; } jvalue;
1. 呼叫例項方法(instance method):
-
<type> Call<type>Method(jobject obj, jmethodID methodID, ...);
呼叫一個具有<type>
型別返回值的方法。 - <type> Call<type>MethodV(jobject obj, jmethodID methodID, va_list args);
- Call<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)
2. 呼叫靜態方法(static method):
<type> CallStatic<type>Method(jobject obj, jmethodID methodID, ...); <type> CallStatic<type>MethodV(jobject obj, jmethodID methodID, va_list args); CallStatic<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)
3. 呼叫父類方法(super.method),這個就有點不一樣了。多了一個 jclass 引數,jclass 可以使 obj 的父類,也可以是 obj 自己的class,但是 methodID 必須是從 jclass 獲取到的,這樣就可以呼叫到父類的方法。
<type> CallNonvirtual<type>Method(jobject obj, jclass clazz, jmethodID methodID, ...) <type> CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); <type> CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args);