Java編程--關於JNI你應該知道的一切,Java編程--JNI
出於效率的問題,很多情況下,我們需要在上層的Java代碼中調用底層 C或C++實現,這時jni就可以大顯身手了。jni(Java Native Interface)允許Java代碼和其他語言寫的代碼進行互動,使用java與本地已編譯的代碼互動,通常會喪失平台可移植性。但是,有些情況下這樣 做是可以接受的,甚至是必須的。例如,使用一些舊的庫,與硬體、作業系統進行互動,或者為了提高程式的效能。
使用JNI編程的步驟
關於javah:在命令視窗中運行javah -h 命令我們可以看到javah的所有選項,這裡簡單介紹一下:
- -classpath,用於裝入類的路徑
- -d,輸出目錄
- -o,輸出檔案, -d和-o只能使用其中之一
- -jni,產生jni樣式的標頭檔(預設)
編寫第一個JNI程式
看到了上面的步驟我們來親自實踐一下,利用jni編寫一個簡單的hello world程式。
1.首先,我們用Java代碼編寫一個本地方法hello:
package com.example.jnitest;public class JniTest { public static native void hello();}
2.接下來我們利用javah產生標頭檔,需要注意的是我們應該首先編譯該工程得到.class檔案。然後我們運行命令列來到工程目錄/bin目錄 下,輸入命令 javah -classpath . com.example.jnitest.JniTest 得到標頭檔,
得到標頭檔的代碼如下:
/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class com_example_jnitest_JniTest */#ifndef _Included_com_example_jnitest_JniTest#define _Included_com_example_jnitest_JniTest#ifdef __cplusplusextern "C" {#endif/* * Class: com_example_jnitest_JniTest * Method: hello * Signature: ()V */JNIEXPORT void JNICALL Java_com_example_jnitest_JniTest_hello (JNIEnv *, jclass);#ifdef __cplusplus}#endif#endif
3.接下來我們要用C語言實現我們的程式,為了方便建立共用庫檔案,在這裡我在VS2012中建立了一個控制台工程,並在應用程式設定中選擇dll和空項目。
接著將剛剛得到的標頭檔添加進來並建立一個cpp檔案來實現我們的程式。
#include"jni_hello.h"JNIEXPORT void JNICALL Java_com_example_jnitest_JniTest_hello (JNIEnv *, jclass){ printf("Hello Jni!\n");}
注意問題,如果我們直接將標頭檔添加進來可能會找不到標頭檔jni.h,這時我們需要配置一下我們的工程,右鍵 工程–>屬性–>VC++目錄,選擇包含目錄–>編輯,將jni標頭檔的目錄添加進來,一般需要添加的目錄是jdk目錄下的 include檔案夾以及include/win32檔案夾,
4.產生共用庫檔案,接下來我們只需要運行該工程即可在Debug檔案夾下找到.dll檔案,需要注意的是,vs直接產生的是32位的dll檔案,如果你 的機器是64位就會報錯Can’t load IA 32-bit .dll on a AMD 64-bit platform,這時我們就用配置一下自己的VS工程:產生–>組態管理員的活動解決方案平台,活動解決方案平台–>建立–>選擇 x64即可
接下來我們需要將我們的共用庫添加進來:
- 首先我們調用System.out.println( System.getProperty(“java.library.path”));獲得共用庫的路徑
- 接下來我們將dll檔案拷貝到其中之一的路徑中
5.載入共用庫,調用native方法:
package com.example.jnitest;public class Main { static{ System.loadLibrary("JniApplication");//靜態初始化塊載入庫 } public static void main(String[] args) { JniTest.hello(); }}
在控制台中輸出結果:
Jni中資料類型對應關係
瞭解了jni的簡單用法,我們再來進行深入的學習:首先使jni中Java基本類型和C基本類型的對應關係
除了基本的資料類型外,比較常用的當屬字串類型,Java中的String對應C中為jstring,我們可以使用如下方法將jstring類型和char *的相互轉化
char *ff = "Hello Jni!"; //建立一個jstring類型字串 jstring j_string = env->NewStringUTF(ff); //將jstring類型轉化為char* const char * c_string = env->GetStringUTFChars(j_string, 0); printf(c_string); //當我們不再使用字串時,要將其釋放 env->ReleaseStringUTFChars(j_string, c_string);
訪問Java中的域
使用C語言訪問Java中的域和方法都要知道其Id,使用起來相對也比較複雜,在這裡舉一個簡單的例子,假設我們的類People中有一個int類型的域age,即:
package com.example.jnitest;public class People { private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; }}
修改JniTest中的方法,這一次我們通過調用native方法使得age域變為原來的二倍:
package com.example.jnitest;public class JniTest { public static native People changeAge(People people);}
重建標頭檔後再cpp檔案中編寫我們的方法,每次訪問Java中的域我們需要得到FieldId才可以,其步驟為:
1. 擷取隱式參數的類
2. 擷取域Id
3. 訪問域的值
#include"jni_hello.h"JNIEXPORT jobject JNICALL Java_com_example_jnitest_JniTest_changeAge (JNIEnv *env, jclass cl, jobject ob){ jclass cl_people = env->GetObjectClass(ob); jfieldID c_ageId = env->GetFieldID(cl_people, "age", "I"); jint c_age = env->GetIntField(ob,c_ageId); //列印修改之後的語句 printf("%d", c_age); //修改age域 env->SetIntField(ob, c_ageId, 2*c_age); return ob;}
env->GetFieldID方法中的第二個參數表示該域的名稱,第三個參數表示該age域的類型為int,這個jni的編碼簽名有關,稍後會提到。接下來在main方法中調用:
package com.example.jnitest;public class Main { static{ System.loadLibrary("JniApplication"); } public static void main(String[] args) { People people = new People(); people.setAge(5); people = JniTest.changeAge(people); System.out.println("changed::"+people.getAge()); }}
輸出結果:
可以看到,結果變為原來的2倍。對於訪問靜態域時只需更換env->GetIntField為env->GetStaticIntField即可。
訪問Java中的方法
剛剛我們使用了訪問域的方法去修改了域的值,下面我們在cpp檔案中調用People類下的setAge方法修改age的值;調用Java的方法需要按照以下步驟:
#include"jni_hello.h"JNIEXPORT jobject JNICALL Java_com_example_jnitest_JniTest_changeAge (JNIEnv *env, jclass cl, jobject ob){ jclass cl_people = env->GetObjectClass(ob); jfieldID c_ageId = env->GetFieldID(cl_people, "age", "I"); jint c_age = env->GetIntField(ob,c_ageId); //第三個參數同樣和編碼簽名有關 jmethodID c_setAgeId = env->GetMethodID(cl_people, "setAge","(I)V"); //void表示了傳回型別,若返回為int,則調用CallIntMethod env->CallVoidMethod(ob,c_setAgeId,2*c_age); return ob;}
在main函數中調用,代碼同上,結果如下:
對於static方法的調用,我們只需要將調用時的方法更換就好,例如setAge方法為static的話,只需將 env->CallVoidMethod(ob,c_setAgeId,2*c_age);更換為 env->CallStaticVoidMethod(ob,c_setAgeId,2*c_age);即可。
Jni編碼簽名
在前面我們提到了env->GetFieldID方法中的第三個參數用編碼簽名來表示了該域的資料類型,現在來介紹一下jni中資料類的對應關係。
- 基礎資料型別 (Elementary Data Type):
B——byte, C——char, D——double, F——float,
I——-int , J——long, S——short, V——–void , Z——boolean
- 數群組類型,在開頭加“[”來表示,如int[] 數組為“[I”;二維數組int[][] 即為“[[I”
- 類類型,以“L+完整包名+類名+;”表示其中所有的“.”換為“/”,如com.example.jnitest.People類表示為Lcom/example/jnitest/People;
關於方法的編碼簽名規則是,“(傳入參數類型的編碼簽名)+返回參數類型的編碼簽名”,多個傳入參數之間直接相接就好;例如我們的setAge方法中傳入參數為int,返回為void,則表示為“(I)V”。