1、JNI是什么
JNI是Java Native Interface的縮寫,它提供若干的API實現Java與其他語言之間的通信。而Android Framework由基於Java語言的的Java層與基於C/C++語言的C/C++層組成,每個層中的功能模塊都是以有相應的語言編寫,並且兩層中的大部分模塊有着千絲萬縷的聯系。而在兩層之間充當連接橋梁這一角色的就是JNI,它允許Java代碼和C/C++編寫的應用程序與庫之間進行交互;通常在以下幾種情況下使用JNI
1、注重處理速度,C/C++的處理速度要優於Java語言
2、硬件控制,硬件驅動程序通常使用C語言編寫,而要是Java層能夠控制硬件,需要用到JNI
3、C/C++代碼的復用,一些好的C/C++模塊可以被多處復用
2、在Java中調用C庫函數
下面以一個例子來說明在Java代碼中調用C庫函數的流程
1、編寫Java代碼
class HelloJNI { /*聲明本地方法,該函數在C庫中實現*/ native void printHello(); native void printString(String str); /*在靜態塊中加載C庫,可以保證在main方法前加載完成*/ static { System.loadLibrary("./hellojni"); } public static void main(String args[]) { HelloJNI myJNI = new HelloJNI(); /*調用C庫中實現的函數*/ myJNI.printHello(); myJNI.printString("Hello world from printstring func"); } }
在上述代碼中使用native關鍵字聲明本地方法,告訴Java編譯器,此函數由其他語言編寫;在靜態塊中加載hellojni庫,該庫由C語言實現,
如果是在Linux系統下則會加載libhellojni.so,如果在Windows系統下則會加載hellojni.dll;(本文以Linux系統為測試環境)
2、編譯Java代碼
javac HelloJNI.java
編譯Java代碼很簡單,只要配置好JDK就可以完成編譯,需要注意的是此時編譯通過,但如果運行的話,由於沒有實現本地函數,所以會拋出找不到函數的異常
3、生成C頭文件
當Java調用本地函數printHello或者printString時並非直接映射到C語言的printHello或者printString函數,而是有一套自己的映射方法,使用如下命令即可生成C函數的頭文件
javap HelloJni
執行完成后生成HelloJni.h如下
/* DO NOT EDIT THIS FILE - it is machine generated */ #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 * Method: printString * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_HelloJNI_printString(JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif 可以看到生成的函數原型並非與Java代碼調用的函數一致,函數有JNIEXPORT和JNICALL兩個關鍵字聲明,這兩個關鍵詞是必須的,有了他們JNI才能正常調用函數;而通過觀察函數名稱,我們可以知道其命名方式是"Java_類名_本地方法名"; 再看參數,可知JNIEnv*和jobject是本地函數的共同參數,第一個參數是JNI接口的直接,用來調用JNI提供的基本函數集;第二個參數中保存着調用本地方法的對象的一個引用,上例中的jobject中保存的對象myJNI的引用,其他的參數根據Java代碼的本地方法的調用生成的 4、編寫C/C++代碼 把上一步驟生成的HelloJni.h頭文件include進來,實現其聲明的函數即可,編寫hellojni.c如下 #include "HelloJNI.h" #include <stdio.h> /* * Class: HelloJNI * Method: printHello * Signature: ()V */ JNIEXPORT void JNICALL Java_HelloJNI_printHello(JNIEnv *env, jobject obj) { printf("Hello World!\n"); return; } /* * Class: HelloJNI * Method: printString * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_HelloJNI_printString(JNIEnv *env, jobject obj, jstring string) { /*JNI提供的基本函數集,將jstring轉化成char *類型*/ const char *str = (*env)->GetStringUTFChars(env, string, 0); printf("%s! \n" ,str); return ; }
5、生成C動態鏈接庫
gcc -fPIC -shared -o libhellojni.so hellojni.c -I$JAVA_HOME/include
其中JAVA_HOME已經配置到環境變量中,表示JDK安裝的目錄,需要指定其中的include目錄使用jni.h頭文件
6、運行Java程序
此時執行java HelloJni會提供找不到hellojni庫,這是由於在加載C庫的時候在默認目錄中沒有找到libhellojni.so庫,只需將該庫復制到/usr/lib/下再次執行
xlzh@cmos:~/code/jni/simpleJNI$ java HelloJNI Hello World! Hello world from printstring func!
3、調用JNI函數
上圖來自<Android框架揭秘>
由上圖可知此示例程序有JniFuncMain類、JniTest類和libjnifunc.so(linux系統)組成,此示例有Java和C代碼混合而成。
JniFuncMain類:
public class JniFuncMain { private static int staticIntField = 300; /*加載libjnifunc.so庫*/ static { System.loadLibrary("jnifunc"); } /*使用static關鍵字聲明本地方法,再C庫中實現*/ public static native JniTest createJniObject(); public static void main(String[] args) { System.out.println("[Java] createJniObject() call native method"); /*調用C庫的createJniObject,得到JniTest對象,注意不是用new*/ JniTest jniObj = createJniObject(); /*利用JniTest對象調用JniTest中的方法*/ jniObj.callTest(); } }
此例中與上例不同的是本地方法返回了一個JniTest類的對象的引用,這樣就可以在JniFuncMain類中調用JniTest類的方法。
JniTest類
class JniTest { private int intField; public JniTest(int num) { intField = num; System.out.println("[Java] call JniTest: intFiled" + intField); } public int callByNative(int num) { System.out.println("[Java] JniTest 對象的 callByNative(" + num + ")調用"); return num; } public void callTest() { System.out.println("[Java] JniTest對象的callTest() 方法調用: intField = " + intField); } }
JniTest類提供兩個方法供JniFuncMain類和C庫函數調用
JniFuncMain.h
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class JniFuncMain */ #ifndef _Included_JniFuncMain #define _Included_JniFuncMain #ifdef __cplusplus extern "C" { #endif /* * Class: JniFuncMain * Method: createJniObject * Signature: ()LJniTest; */ JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif
使用javah JniFuncMain生成頭文件,需要注意的是第二個參數是jclass,而不是jobject,這是由於該本地方法在JniFuncMain類中聲明的是static方法,所以第二個參數表示的該類的應用,而不需要對象的引用
jnifunc.cpp #include "JniFuncMain.h" JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *env, jclass clazz) { jclass targetClass; jmethodID mid; jobject newObject; jstring hellostr; jfieldID fid; jint staticIntField; jint result; /*獲取JniFuncMain類的staticField變量值*/ fid = env->GetStaticFieldID(clazz, "staticIntField", "I"); staticIntField = env->GetStaticIntField(clazz, fid); printf("[CPP] 獲取 JniFuncMain類的staticIntField 值\n"); printf(" JniFuncMain.staticIntField = %d\n", staticIntField); /*查找生成對象的類*/ targetClass = env->FindClass("JniTest"); /*查找構造方法*/ mid = env->GetMethodID(targetClass, "<init>", "(I)V"); /*生成JniTest對象*/ printf("[CPP] JniTest 對象生成 \n"); newObject = env->NewObject(targetClass, mid, 100); /*調用對象的方法*/ mid = env->GetMethodID(targetClass, "callByNative", "(I)I"); result = env->CallIntMethod(newObject, mid, 200); /*設置JniObject對象的intField值*/ fid = env->GetFieldID(targetClass, "intField", "I"); printf("[CPP] 設置JniTest對象的intField值為200\n"); env->SetIntField(newObject, fid, result); /*返回對象引用*/ return newObject; } 如果想在C代碼中訪問Java中的成員變量,就需要獲取相應成員變量的ID值,成員變量的ID值保存在jfieldID類型的變量中;獲取成員變量ID的JNI本地方法有兩個,分別是 /* 獲取Java中的靜態成員變量ID * env : JNI接口指針 * clazz : 包含成員變量的類的jclass * name : 成員變量名稱 * signature: 成員變量簽名 */ jfield GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *signature); /*獲取Java中的普通成員變量ID*/ jfield GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *signature); 上述示例中要訪問Java類中的靜態成員變量,所以需要使用GetStaticFieldID方法,其他參數很簡單,直接使用即可,而對於變量的簽名,則需要借助Java反編譯器javap命令,如下所示 xlzh@cmos:~/code/jni/middleJNI$ javap -s -p JniFuncMain Compiled from "JniFuncMain.java" public class JniFuncMain { private static int staticIntField; Signature: I public JniFuncMain(); Signature: ()V public static native JniTest createJniObject(); Signature: ()LJniTest; public static void main(java.lang.String[]); Signature: ([Ljava/lang/String;)V static {}; Signature: ()V }
可以看到,staticIntField的簽名是I,將I傳入第四個參數即可,其他函數中用到簽名的時候可用同樣的方法獲取
OK,我們得到了成員變量的ID,那么如何通過成員變量的ID來獲取或者設置成員變量的值呢?就需要用到以下幾個JNI函數
/* * 獲取Java類中靜態成員變量的值 * <jnitype> jobject,jboolean,jbyte,jchar, jshort, jint, jlong, jfloat, jdouble * <type> Object ,Boolean ,Byte,Char , Short , Int, Long , Float , Double * env: JNI接口指針 * jcalss: 包含成員變量的類 * jfieldID: 成員變量ID */ <jnitype> GetStatic<type>Field(JNIEnv *env, jcalss jclazz, jfieldID fieldID) /* * 獲取Java類的對象中普通成員變量的值 */ <jnitype> Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID) /* * 設置Java類中靜態成員變量的值 */ <jnitype> SetStatic<type>Field(JNIEnv *env, jcalss clazz, jfieldID fieldID, <type> value) /* * 設置Java類的對象中普通成員變量的值 */ <jnitype> Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, <type> value) 與成員變量類似, 獲取和調用類中方法的JNI函數原型如下 /*獲取Java類靜態方法的ID*/ jmethod GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *signature) /*獲取Java類的對象中普通方法的ID*/ jmethod GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *signature) /*調用Java類中的靜態方法*/ <jnitype> CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethod methodID, ...) /*調用Java類的對象中的普通方法*/ <jnitype> Call<type>Method(JNIEnv *env, jobject obj, jmethod methodID, ...) 如何獲取Java類的對象呢?以示例中獲取JniTest類的對象代碼例,分為三步 1、獲取JniTest的類 2、獲取JniTest類的構造方法ID 3、通過構造方法的ID調用和JniTest類使用NeoObject方法生成對象 對比上例中獲取JniTest的對象流程,可以很清楚的進行對照
4、在C代碼中運行Java類
Java類編譯的字節碼需要在Java虛擬機上運行,那么在C/C++中運行Java類自然也需要加載Java虛擬機;JNI為我們提供了一套Invocation API,它允許本地代碼在自身內存區域內加載Java虛擬機,同樣我們以實例的方式進行講解
InvocationApiTest.java
public class InvocationApiTest { public static void main(String[] args) { System.out.println(args[]0); } }
invocationApi.c
#include <jni.h> int main(void) { JNIEnv *env; JavaVM *vm; JavaVMInitArgs vm_args; JavaVMOption options[1]; jint res; jclass cls; jmethodID mid; jstring jstr; jclass stringClass; jobjectArray args; /*加載虛擬機選項*/ options[0].optionString = "-Djava.class.path=."; vm_args.version = JNI_VERSION_1_6; vm_args.options = options; vm_args.nOptions = 1; vm_args.ignoreUnrecognized = JNI_TRUE; /*生成虛擬機*/ res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args); /*查找並加載類*/ cls = (*env)->FindClass(env, "InvocationApiTest"); /*獲取main()方法的ID*/ mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V"); /*生成字符串對象*/ jstr = (*env)->NewStringUTF(env, "Hello Invocation API!!"); stringClass = (*env)->FindClass(env, "java/lang/String"); args = (*env)->NewObjectArray(env, 1, stringClass, jstr); /*調用main()方法*/ (*env)->CallStaticVoidMethod(env, cls, mid, args); /*銷毀虛擬機*/ (*vm)->DestroyJavaVM(vm); }
編譯允許結果如下
xlzh@cmos:~/code/jni/superJNI$ javac InvocationApiTest.java xlzh@cmos:~/code/jni/superJNI$ sudo echo "/usr/lib/jvm/java-1.7.0-openjdk-amd64/jre/lib/amd64/jamvm" >> /etc/ld.so.conf xlzh@cmos:~/code/jni/superJNI$ sudo ldconfig xlzh@cmos:~/code/jni/superJNI$ gcc -o a.out invocationApi.c -I$JAVA_HOME/include -L$JAVA_HOME/jre/lib/amd64/jamvm/ -ljvm xlzh@cmos:~/code/jni/superJNI$ ./a.out Hello Invocation API!! xlzh@cmos:~/code/jni/superJNI$