在Android Framework中,需要提供一種媒介或橋梁,將Java層(上層)與C/C++(底層)有機地聯系起來,使得它們相互協調,共同完成某些任務。在這兩層之間充當連接橋梁這一角色的就是Java本地接口(JNI,Java Native Interface),它允許Java代碼與基於C/C++編寫的應用和庫進行交互操作。
JNI提供了一系列接口,允許Java類與使用C/C++等其它編程語言(在JNI中,這些語言被稱為本地語言)編寫的應用程序、模塊、庫進行交互操作。比如,在Java類中使用C語言庫中中的特定函數,或在C語言里面使用Java類庫,都需要借助JNI來完成。
通常會在下列幾種情況下使用JNI
- 注重處理速度:如果對某段程序的執行速度有較高的要求,建議使用C/C++編寫代碼,而后在Java層通過JNI調用基於C/C++編寫的部分代碼。
- 硬件控制:為了更好地控制硬件,硬件控制代碼通常使用C語言編寫,借助JNI將其與Java層連接起來,從而實現對硬件的控制。
- 已有C/C++代碼的復用:在編寫程序的過程中,常常會使用已經編寫好的C/C++代碼,既提高了編程效率,又確保了程序的安全性和健壯性。在復用這些C/C++代碼時,就要通過JNI來實現。
在Java代碼中通過JNI調用C函數的步驟如下:
- 第一步:編寫Java代碼
- 第二步:編譯Java代碼(javac Java文件)
- 第三步:生成C代碼頭文件(javah java類名,自動生成)
- 第四步:編寫C代碼(實現C代碼頭文件里面的函數)
- 第五步:生成C共享庫(使用工具編譯生成C共享庫,win下面為dll文件,Linux下面為so文件)
- 第六步:運行Java程序(java 類名)
第一步:編寫Java代碼
首先編寫調用C語言的Java源代碼HelloJNI.java
public class HelloJNI { native void printHello(); 1 native void printString(String str); static{ System.loadLibrary("hellojni"); 2 } public static void main(String[] args) { // TODO Auto-generated method stub HelloJNI myJNI = new HelloJNI(); myJNI.printHello(); myJNI.printString("Hello world form printString function!"); } }
說明:
1在Java類中,使用”native”關鍵字,聲明本地方法,該方法與用C/C++編寫的JNI本地函數相對應。”native”關鍵字告知Java編譯器,在Java代碼中帶有該關鍵字的方法只是聲明,具體由C/C++等其它語言編寫實現。
2在Java類中聲明了本地方法之后,接下來,調用System.loadLibrary()方法,加載具體實現本地方法的C運行庫(在Java中加載本地運行庫通常使用靜態塊(static block))。System.loadLibrary()方法加載由字符串參數指定的本地庫,在不同操作系統平台下,加載的C運行庫不同。在Window下面,調用System.loadLibrary(“hellojni”),則hellojni.dll會被加載;在Linux下面,則會加載libhellojni.so文件。
第二步:編譯Java代碼
使用如下命令編譯java源代碼:
javac HelloJNI.java
編譯好HelloJNI.java后,生成HelloJNI.class文件。如果此時直接運行java程序,就是拋出異常。
由於尚未創建加載到Java代碼中的hellojni.dll庫文件,無法找到Java虛擬機要加載的C運行庫。
接下來,創建hellojni.dll庫文件。
第三步:生成C代碼頭文件
若想創建本地方法的映射C函數,必須先生成函數原型,函數原型存在於C/C++頭文件中。Java提供了javah工具,位於JAVA JDK的安裝目錄的bin目錄下面,用來生成包含函數原型的C/C++頭文件,使用方法如下:
javah <包含以native關鍵字聲明方法的Java類名稱>
運行javah命令,會在當前目錄下生成與Java類名(即javah命令的參數)相同名稱的C語言頭文件。在生成的C頭文件中,定義了與Java本地方法相鏈接的C函數原型。
以下為生成的HelloJNI.h文件內容:
/* DO NOT EDIT THIS FILE - it is machine generated */ 1 #include <jni.h> /* Header for class HelloJNI */ #ifndef _Included_HelloJNI #define _Included_HelloJNI #ifdef __cplusplus extern "C" { #endif /* * Class: HelloJNI * Method: printHello * Signature: ()V */ JNIEXPORT void JNICALL Java_HelloJNI_printHello (JNIEnv *, jobject); /* * Class: HelloJNI 2 * Method: printString * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_HelloJNI_printString (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
說明:
1該文件由javah命令生成,為了保證JNI正常運行,請不要直接修改本文件的內容,JNI開發者只要使用C/C++語言實現定義的函數即可。
2這是javah命令生成的兩個C函數原型,函數原型在Java類中聲明的本地方法的基礎上生成。查看各函數原型注釋,可以看到與各函數原型對應的Java代碼中的本地方法,注釋中標明了三個元素:類名、本地方法、本地方法簽名。
接下來分析一下函數原型:JNIEXPORT、JNICALL都是JNI關鍵字,表示此函數要被JNI調用,函數原型中必須有這兩個關鍵字,JNI才能正常調用函數。其實,JNIEXPORT、JNICALL兩個關鍵字都是宏定義,在JDK_HOME/include/win32/jni_md.h文件(在Window平台下)中。
觀察函數原型名稱,可以發現函數名稱遵循一定的命名規則:JNI支持的函數命名形式為”Java_類名_本地方法名”。通過函數命名即可推斷出JNI本地函數與哪個Java類的哪個本地方法相對應。
在生成的函數原型中,帶有兩個默認參數,分別為JNIEnv * 與jobject,支持JNI的函數必須包含這兩個共同參數。第一個參數JNIEnv *為JNI接口指針,用來調用JNI表中的各種JNI函數(這里的JNI函數是指JNI中提供的基本函數集);第二個參數jobject也是JNI提供的Java本地方法,用來在C代碼中訪問Java對象,此參數中保存着調用本地方法的對象的一個引用。
在JNI編程中,Java程序與C/C++函數間經常進行數據交換,如果不提供一種方法消除兩種語言的數據類型的差異,那么程序就無法正常運行,運行的可靠性也無法保障。JNI提供了一套與Java數據類型相對應的Java本地類型,使得本地語言可以使用Java數據類型,如下表所示:
Java類型 |
本地類型 |
字節(bit) |
boolean |
jboolean |
8, |
byte |
jbyte |
8 |
char |
jchar |
16, |
short |
jshort |
16 |
int |
jint |
32 |
long |
jlong |
64 |
float |
jfloat |
32 |
double |
jdouble |
64 |
void |
void |
n/a |
以上Java本地類型定義在JDK_HOME/include/jni.h文件中。此外,Java本地類型也提供了另外三種類型,分別對應於Java類、對象與字符串三種引用類型數據(除此之外還有幾種引用類型,有興趣可以可以查閱相關資料)。
Java引用類型 |
Java本地類型 |
類 |
Jclass |
對象 |
Jobject |
String |
Jstring |
第四步:編寫C代碼
在C函數原型生成后,開始編寫hellojni.c文件,具體實現JNI本地函數。首先,把定義在HelloJNI.h頭文件中的函數原型復制到hellojni.c,注意在使用javah命名生成的頭文件中,函數的參數僅指定了參數的類型,並未給出參數的名稱,因此復制完函數原型,開始實現C函數時,必須先在參數類型后指定具體的參數名稱。
以下為編寫好的hellojni.c代碼:
#include "HelloJNI.h" #include <stdio.h> //添加名稱為env與obj的兩個參數 JNIEXPORT void JNICALL Java_HelloJNI_printHello(JNIEnv *env, jobject obj) { printf("Hello world!\n"); return ; } JNIEXPORT void JNICALL Java_HelloJNI_printString(JNIEnv *env, jobject obj, jstring string) { //將Java String轉換以C字符串 const char * str = (*env)->GetStringUTFChars(env,string,0); printf("%s!\n",str); return ; }
GetStringUTFChars ()是JNI函數,用來將Java字符串轉換成C語言字符串。JNI提供了多種JNI函數,用來處理C字符串與Java字符串的轉換,具體可以參考:
第五步:生成C共享庫
在編寫好了hellojni.c之后,使用編譯器將其編譯成hellojni.dll文件。這里使用的是Visual C++ 2008 Express Editions。安裝好之后,使用Visual Studio 2008 命令提示,輸入編譯指令:
cl -I"F:\Java\jdk1.7.0\include" -I"F:\Java\jdk1.7.0\include\win32" -LD hellojni.c -Fehellojni.dll
執行結果為:
指令說明:
cl:visual c++編譯命令
-I<dir>:添加要檢索頭文件的目錄路徑<dir>
為了檢索頭文件,添加如下目錄
jni.h(<JDK_HONE>\include)
jni_md.h(JDK_HONE>\include\win32)
-LD:創建DLL
-Fe<文件名>:指定編譯結果文件名稱
第六步:運行Java程序
執行java指令,運行HelloJNI類。
附錄:
生成JNI的DLL時提示找不到jni.h的解決辦法Cannot open include file: 'jni.h': No such file or directory