** 在Java中調用C庫函數 開發流程 ------ 在Java代碼中通過JNI調用C函數的步驟如下: 第一步: 編寫Java代碼 第二步: 編譯Java代碼 第三步: 生成C語言頭文件 第四步: 編寫C代碼 第五步: 生成C共享庫 第六步: 運行Java程序 *** 第一步 編寫Java代碼 JNI方法是在Java代碼中聲明的。 在Java類中,使用"native"關鍵字,聲明本地方法該方法與用C/C++編寫的JNI本地函數相對應。"native"關鍵字告知Java編譯器,在Java代碼中帶有該關鍵字的方法只是聲明,具體由C/C++等其他語言編寫實現。 如果起吊方法前的native關鍵字,編譯代碼時,Java編譯器就會報錯,拋出編譯錯誤,告知該方法沒有實現。 調用System.loadLibrary()方法加載具體的實現本地方法的C運行庫。System.loadLibrary()方法加載由字符串參數指定的本地庫,在不同操作系統平臺下,加載的C運行庫不同。 *** 第二步 編譯Java代碼 #+BEGIN_SRC java javac xxx.java #+END_SRC 生成 xxx.class *** 第三步 生成C語言頭文件 #+BEGIN_SRC java javah -classpath path classname #+END_SRC 生成classname.h | Java類型 | Java本地類型 | |----------+--------------| | / | < | |----------+--------------| | byte | jbyte | | short | jshort | | int | jint | | long | jlong | | float | jfloat | | double | jdouble | | char | jchar | | boolean | jboolean | | void | void | Java本地類型也提供了另外三種類型 | java引用類型 | java本地類型 | |--------------+--------------| | / | < | |--------------+--------------| | 對象 | Jobject | | String | Jstring | *** 第四步 編寫C/C++代碼 編寫xxx.c文件 *** 第五步 生成C共享庫 #+BEGIN_SRC sh cc -I/usr/lib/jvm/java-6-sun/include/linux -I/usr/lib/jvm/java-6-sun/include/ -fPIC -shared -o libxxx.so xxx.c #+END_SRC *** 第六步 運行Java程序 #+BEGIN_SRC java java -cp path -o java.library.path='path' classname #+END_SRC ** 小結 (1)在java類中聲明本地方法 (2)使用javah命令,生成包含JNI本地函數原型的頭文件 (3)實現JNI本地函數 (4)生成C共享庫 (5)通過JNI,調用JNI本地函數 * 調用JNI函數 在由C語言編寫的JNI本地函數中如何控制Java端的代碼 - 創建Java對象 - 訪問靜態成員域 - 調用類的靜態方法 - 訪問Java對象的成員變量 - 訪問Java對象的方法 ** 調用JNI函數的示例程序結構 ** Java層代碼 (JniFuncMain.java) 1.JniFuncMain類 #+BEGIN_SRC java public class JniFuncMain { print static int staticIntField = 300; // 加載本地庫 static { System.loadLibrary("jnifunc"); } // 本地方法聲明 public static native JniTest createJniObject(); public static void main(String[] args) { // 從本地代碼生成JniTest對象 System.out.println("[Java] createJniObject() 調用本地方法"); JniTest jniObj = createJniObject(); // 調用JniTest對象的方法 jniObj.callTest(); } } #+END_SRC JniFuncMain.java中的JniFuncMain類 + 通過java靜態塊,在調用本地方法前,加載jnifunc運行庫 + 使用static關鍵字聲明本地方法createJniObject()在調研那個此方法時不需要創建對象,直接通過JniFuncMain類調用即可 + 不使用Java語言的new運算符,調用與createJniObject()本地方法相對應的C函數生成JniTest類的對象,在將對象的引用保存在jniObj引用變量中 + 調用jniObj對象的callTest()方法 2.JniTest類 #+BEGIN_SRC java class JniTest { private int intField; //構造方法 public JniTest(int num) { intField = num; System.out.println("[Java] 調用JniTest對象的構造方法:intField = " + intField); } // 此方法由JNI本地函數調用 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"); } } #+END_SRC ** 分析JNI本地函數代碼 **** JniFuncMain.h頭文件 使用javah命令,生成本地方法的函數原型 #+BEGIN_SRC java javah JniFuncMain #+END_SRC JniFuncMain.h #+BEGIN_SRC c /* 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 #+END_SRC createJniObject()本地方法對應的JNI本地函數原型,形式如下 JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *, jclass) **** jnifunc.cpp 文件 #+BEGIN_SRC C++ 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類的staticIntField變量值 fid = env->GetStaticFieldID(clazz, "staticIntField", "I"); staticIntField = env->GetStaticIntField(clazz, fid); printf("[CPP] 獲取JniFuncMain類的staticIntField值\n"); printf(" JniFuncMain.staticIntField = %d\n", staticIntField); // 查找生成對象的類 targetClass = new->NewObject(targetClass, mid , 100); // 查找構造方法 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; } #+END_SRC **** 通過JNI,獲取成員變量值 下面代碼用於獲取JniFuncMaind類的staticIntField成員變量的值 #+BEGIN_SRC c // 1. 查找含有待放文成員變量的JniFuncMain類的jclass值 // 2. 獲取staticField變量的ID值 fid = env->GetStaticFieldID(clazz, "staticIntField", "I"); // 3. 讀取jclass與fieldid指定的成員變量值 staticIntField = env->GetStaticIntField(clazz, fid); #+END_SRC 程序通過JNI訪問java類/對象的成員變量安如下順序進行: (1) 查找含待放文的成員變量的Java類的jclass值 (2) 獲取此類成員變量的jfieldID值。若成員變量爲靜態變量,則調用名稱爲GetStaticFieldID()的JNI函數;若待訪問的成員變量是普通對象,則調用名稱爲GetFieldID()的JNI函數。 (3) 使用12中獲得的jclass與jfieldID值,獲取或設置成員變量值。 依據以上順序,待讀取樹脂的staticIntField成員變量在JniFuncMain類被聲明。JniFuncMain類的jclass值被傳遞給JNI本地函數 =java_JniFuncMain_createJniObject()= 的第二個參數中,若想獲取指定類的jclass值,調用JNI函數FindClass()即可。 若想在本地代碼中訪問Java的成員變量,必須獲取相應成員變量的ID值。例子中成員變量的ID保存在jfieldID類型的變量中。由於待讀取數值的staticIntField成員變量時JniFUncMain類的靜態變量,在獲取staticIntField的ID時,影調用名稱爲GetStaticFieldID()的JNI函數。 在例子中的GetStaticFieldID()函數,與下表中的GetStaticFieldID()函數原型有些不同,函數原型中帶有四個參數,而代碼中僅有三個,缺少了env參數,這不是錯誤,而是與所用的編程語言相關。具體請參考後面Tip中關於JNI函數編碼風格的說明。 | JNI函數 - GetStaticFieldID() | | |------------------------------+--------------------------------------------------------------------------------------------| | / | < | | 形式 | jfield GetStaticFieldID(JNIEnv *env, jclass clazz, const char*name, const char *signature) | |------------------------------+--------------------------------------------------------------------------------------------| | 說明 | 返回指定類的指定的靜態變量的jfieldID的值 | |------------------------------+--------------------------------------------------------------------------------------------| | 參數 | env-JNI接口指針 clazz-包含成員變量的類的jclass name-成員變量名 signature-成員變量簽名 | | JNI函數 - GetFieldID() | | |------------------------+---------------------------------------------------------------------------------------| | / | < | | 形式 | jfield GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *signature) | |------------------------+---------------------------------------------------------------------------------------| | 說明 | 返回對象中指定的成員變量的jfieldID的值 | |------------------------+---------------------------------------------------------------------------------------| | 參數 | env-JNI接口指針 clazz-包含成員變量的類的jclass name-成員變量名 signatuer-成員變量簽名 | 以上兩個函數都要去提供成員變量的簽名。成員變量與成員方法都擁有簽名,使用 =<JDK_HOME>/bin= 目錄下的javap命令(java反編譯器),可以獲取成員變量活成員方法簽名。 Tip: 在JNI中獲取成員變量活成員方法簽名 形式: javap =[選項]= '類名' 選項: -s 輸出java簽名 -p 輸出所有類及成員 在獲取成員變量所在的類與ID後,根據各個成員變量的類型與存儲區塊(static或non-static),調用相應的JNI函數讀取成員變量值即可。在JNI中有兩種函數用來獲取成員便令的值,分別爲Get<type>Field函數與GetStatic<type> Field函數。<type>指Int, Char, Double等基本數據類型,具體參考JNI文檔。 | JNI函數 GetStatic<type>Field | | |------------------------------+------------------------------------------------------------------------------------------------------| | / | < | | 形式 | <jnitype>GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID) | |------------------------------+------------------------------------------------------------------------------------------------------| | 說明 | 返回clazz類中ID爲fieldID的靜態變量的值 | |------------------------------+------------------------------------------------------------------------------------------------------| | 參數 | env-JNI接口指針 clazz-包含成員變量的類 fieldID-成員變量的ID | |------------------------------+------------------------------------------------------------------------------------------------------| | 參考 | <type>指Object、Boolean、Byte、Char、Short、Int、Long、Float、Double九種基本類型 | | | 返回類型<jnitype>指jobject、jboolean、jbyte、jchar、jshort、jint、jlong、jfloat、jdouble九種基本類型 | |------------------------------+------------------------------------------------------------------------------------------------------| | 返回值 | 返回靜態成員變量的值 | | JNI函數 Get<type>Field | | | / | < | |------------------------+---------------------------------------------------------------------| | 形式 | <jnitype>Get<type>Field(JNIEnv *env, Jobject obj, jfieldID fieldID) | |------------------------+---------------------------------------------------------------------| | 說明 | 返回obj對象中ID爲fieldID的成員變量的值 | |------------------------+---------------------------------------------------------------------| | 參數 | env-JNI接口指針 | | | obj-包含成員變量的對象 | | | fieldID-成員變量的ID | |------------------------+---------------------------------------------------------------------| | 返回值 | 返回成員變量的值 | 由於staticIntField是Int類型的靜態成員變量,所以調用GetStaticFieldID()函數即可獲取StaticIntField的值. 生成對象 在JNI本地函數中如何生成Java類對象呢? ----- // 1. 查找生成對象的類 targetClass = env->FindClass("JniTest"); // 2. 查找類的構造方法 mid = env->GetMethodID(targetClass, "<init>", "(I)V"); // 3. 生成JniTest類對象(返回對象引用) newObject = env->NewObject(targetClass, mid, 100); ----- 通過JNI函數,生成Java對象的順序如下: 1. 查找指定的類,並將查找到的類賦值給jclass類型的變量。 2. 查找java類構造方法的ID值,類型爲jmethodID。 3. 生成java類對象 首先調用JNI函數FindClass(),查找生成對象的類。將類名作爲FindClass()函數參數,查找並獲得jclass值 | JNI函數 FindClass | | |-------------------+-------------------------------------------------| | 形式 | jclass FindClass(JNIEnv *env, const char *name) | |-------------------+-------------------------------------------------| | 說明 | 查找name指定的Java類 | |-------------------+-------------------------------------------------------| | 參數 | env-JNI接口指針 | | | name-待查找的類名 | |-------------------+-------------------------------------------------| | 返回值 | 返回jclass的值 | 獲取類的構造方法的ID並保存在jmethodID變量中。在JNI函數中有一個GetMethodID()函數用來獲取指定類的指定方法ID。此函數除了可以用來獲取指定類的構造方法的ID外,還可以獲取類的其他的方法的ID。若指定的是靜態方法,則可以調用JNI函數中的GetStaticMethodID()函數,獲得指定靜態方法的ID。 | JNI函數 GetMethodID | | |---------------------+-----------------------------------------------------------------------------------------------------------| | 形式 | jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *signature) | |---------------------+-----------------------------------------------------------------------------------------------------------| | 說明 | 獲取clazz類對象的指定方法ID。注意,方法名(name)與簽名應當保持一致。若獲取類構造方法的ID,方法名應爲<init> | |---------------------+-----------------------------------------------------------------------------------------------------------| | 參數 | env: JNI接口指針 | | | clazz:Java類 | | | name:方法名 | | | signature:方法簽名 | |---------------------+-----------------------------------------------------------------------------------------------------------| | 返回值 | 若方法ID錯誤,則返回NULL | 以類的jclass與構造方法ID爲參數,調用函數NewObject()函數生成JniTest類的對象。JniTest類的構造方法JniTest(int num)帶有一個int類型的參數,在調用NewObject()時,同時傳入100這一int數據。在生成類對象後,將對象的引用保存在jobject變量中。 | JNI函數 NewObject | | |-------------------+-----------------------------------------------------------------------| | 形式 | jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...) | |-------------------+-----------------------------------------------------------------------| | 說明 | 生成指定類的對象。methodID指類的構造方法的ID | |-------------------+-----------------------------------------------------------------------| | 參數 | env:JNI接口指針 | | | clazz: Java類 | | | methodID:類的構造方法的ID | | | ...:傳遞給類構造方法的參數 | |-------------------+-----------------------------------------------------------------------| | 返回值 | 返回類對象的引用。若發生錯誤,返回NULl | Tip: 局部引用與全局引用 在實現JNI本地函數時,由GetObjectClass()、FindClass()等JNI函數返回的jclass\jobject等引用都是局部引用(Local Reference) 局部引用是JNI默認的,它僅在JNI本地函數內部有效,即當JNI本地函數返回後,其內部的引用就會失效。 在JNI編程中,實現JNI本地函數時,必須準確地理解局部引用的含義。 下面再舉一個例子進一步詳細的說明一下。 #+BEGIN_SRC java class RefTest { public static int intField; public static void setField(int num) { int Field = num; } } public class RefTestMain { // 加載本地庫 static { System.loadLibrary("reftest"e); } // 聲明本地方法 public static native int getMember(); public static void main(String[] args) { RefTest.setField(100); System.out.println("intField = " + getMember()); RefTest.setField(200); System.out.println("intField = " + getMember()); } } #+END_SRC 其中,本地方法getMember()的具體實現在reftest.cpp中。爲了說明局部引用問題,聲明瞭一個靜態jclass變量targetClass,準備保存類的引用。 #+BEGIN_SRC c static jclass targetClass = 0; JNIEXPORT jint JNICALL Java_RefTestMain_getMember(JNIEnv *env, jclass clazz) { jfieldID fid; jint intField; jclass targetClass; if(targetClass == 0) { targetClass = env->FindClass(RefTest"); } fid = env->GetStaticFieldID(targetClass, "intField", "I"); intField = env->GetStaticIntFIeld(targetClass, fid); return intField; } #+END_SRC 運行程序會報錯,原因在於JNI函數中的if (targetClass == 0)的判斷,在java中的兩次調用,第一次調用時targetClass還爲0,第二次就不爲0了。第二次沒有調用FindClass造成出現錯誤。 爲了解決這一問題,JNI提供了一個名爲NewGlobalRef()的JNI函數,用來爲指定的類或對象生成全局引用(Global Reference),以便在JNI本地函數中在全局範圍內使用該引用。 | JNI函數 NewGlobalRef | | |----------------------+------------------------------------------------| | 形式 | jobject NewGlobalRef(JNIEnv *env, jobject obj) | |----------------------+------------------------------------------------| | 說明 | 爲obj指定的類或對象,生成全局引用 | |----------------------+------------------------------------------------| | 參數 | env: JNI接口指針 | | | obj: 待生成全局引用的引用值 | |----------------------+------------------------------------------------| | 返回值 | 返回生成的全局引用,所發生錯誤,返回NULL | 當全局引用使用完後,應當調用名稱爲DeleteGlobalRef()的JNI函數,顯性的將全局引用銷燬。 #+BEGIN_SRC c #include "RefTestMain.h" static jclass globalTargetClass = 0; JNIEXPORT jint JNICALL Java_RefTestMain_getMember (JNIEnv *env, jclass jclazz) { jfieldID fid; jint intField; jclass targetClass; if(globalTargetClass == 0) { targetClass = env->FindClass("RefTest"); globalTargetClass = (jclass)env->NewGlobalRef(targetClass); } fid = env->GetStaticFieldID(globalTargetClass, "initField", "I"); intField = env->GetStaticIntField(globalTargetClass, fid); return intField; } #+END_SRC 上面代碼調用了NewGlobalRef()函數,將targetClass中保存的RefTest類的局部引用(由FindClass()函數返回)轉換成全局引用。並且將生成的全局引用保存在globalTargetClass靜態變量中。 局部引用在函數執行完程後即無效。而全局引用除非調用DeleteGlobalRef()明確將其銷燬,不然這個全局引用總是有效的,可以在運行庫的其他函數中使用該引用。 **** 調用Java方法 下面描述瞭如何使用JNI函數調用Java方法,並將返回值保存至JNI本地函數的變量中的過程 ----- // 1. 獲取含待調用方法的Java類的jclass targetClass = env->GetObjectClass(newObject); // 2. 獲取待調用方法的ID mid = env->GetMethodID(targetClass, "callByNative", "(I)I"); // 3. 調用Java方法 保存返回值 result = env->CallIntMethod(newObject, mid, 200); ----- 通過JNI調用Java方法的順序如下 1. 獲取含待調用方法的Java類的jclass。若待調用方法屬於某個Java類對象,則該方法用來獲取Java類對象的jobject。 2. 調用GetMethodID()函數,獲取待調用方法的ID(jMethodID)。使用jclass與GetMethodID()函數 3. 根據返回值類型,調用相應的JNI函數,實現對Java方法的調用。若待調用的Java方法是靜態方法,則調用函數的形式應爲CallStatic<type>Method();若待調用的方法屬於某個類對象,則調用函數的形式應爲Call<type>Method()。 程序首先獲取含callByNative()方法的JniTest類的jclass。在獲取JniTest類的jclass時,可以直接調用FindClass()函數,將類引用保存在targetClass中。但是爲了向各位介紹GetObjectClass()這個JNI函數,因而在此調用了GetObjectClass()函數。 | JNI函數 CallStatic<type>Method() | | |----------------------------------+------------------------------------------------------------------------------------| | 形式 | <jnitype>CallStatic<type>Method(JNIEnv *env, jcalss clazz,jmethodID methodID, ...) | |----------------------------------+------------------------------------------------------------------------------------| | 說明 |調用methodID指定的類的靜態方法 | |----------------------------------+------------------------------------------------------------------------------------| | 參數 | env: JNI接口指針 | | | clazz: 含待調方法的類 | | | methodID:待調方法的ID 由GetStaticMethodID()函數獲取 | | | ...:傳遞給待調方法的參數 | |----------------------------------+------------------------------------------------------------------------------------| | 返回值 | 被調方法的返回值 | |----------------------------------+------------------------------------------------------------------------------------| | 參考 | <type>除了前面說<Get<type>FieldID()時列出的九種外又添加了void類型,返回值<jnitype>也增加了void類型。 待調方法的返回值不同,<type>也不同。若待調方法的返回值類型爲int, 則調用函數爲CallStaticIntMethod() | | JNI函數 Call<type>Method() | | |----------------------------+-------------------------------------------------------------------------------| | 形式 | <jnitype>Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...) | |----------------------------+-------------------------------------------------------------------------------| | 說明 | 調用methodID指定的java對象的方法 | |----------------------------+-------------------------------------------------------------------------------| | 參數 | env: JNI接口指針 | | | obj: 含待調方法的Java對象的引用 | | | methodID: 待調用方法的ID,由GetMethodID()函數來獲取 | | | ...: 傳遞給待調用方法的參數 | |----------------------------+-------------------------------------------------------------------------------| | 返回值 | 被調用方法的返回值 | **** 通過JNI設置成員變量的值 ----- // 1. 獲取含IntField成員變量的JniTest類的jclass值 // 類引用已經被保存到targetClass中 // 2. 獲取JniTest對象的IntField變量值 fid = env->GetFieldID(targetClass, "intField", "I"); // 3. 將result值設置爲IntField值 env->SetIntField(newObject, fid, resutl); ----- | JNI函數 SetStatic<type>Field | | |------------------------------+-------------------------------------------------------------------------------------| | 形式 | void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, <type>value) | |------------------------------+-------------------------------------------------------------------------------------| | 說明 | 設置fieldID指定的Java類靜態成員變量的值 | |------------------------------+-------------------------------------------------------------------------------------| | 參數 | env: JNI接口指針 | | | clazz: 含待設置成員變量的類的引用 | | | fieldID: 待設成員變量的ID,由GetStaticFieldID()函數獲取 | | | value: 指定設置值 | | JNI函數 Set<type>Field | | |------------------------+--------------------------------------------------------------------------------| | 形式 | void Set<type>Field (JNIEnv *env, jobject obj, jfieldID fieldID, <type> value) | |------------------------+--------------------------------------------------------------------------------| | 說明 | 設置fieldID指定的Java對象的成員變量 | |------------------------+--------------------------------------------------------------------------------| | 參數 | env: JNI接口指針 | | | obj: 包含待設成員變量的Java對象的引用 | | | fieldID: 待設成員變量的ID,由GetFieldID()函數獲取 | | | value:指定設置值 | * 在C程序中運行Java類 本節中學習在由C/C++編寫的主程序中如何運行Java類,這也是使用JNI的重要方式。 在C/C++程序中運行Java類也必須使用Java虛擬機。爲此JNI提供了一套Invocation API,它允許本地代碼在自身內存區域內加載Java虛擬機。 下面列出的可能使你決定使用Invocation API在C/C++代碼中調用Java代碼的集中典型情況: + 需要在C/C++編寫的本地應用程序中訪問用Java語言編寫的代碼或代碼庫 + 希望在C/C++編寫的本地應用程序中使用標準的Java庫 + 當需要把自己已有的C/C++程序與Java程序組織鏈接在一起時,使用Invocation API可以將它們組織成一個完整的程序 ** Invocaton API 應用示例 實例程序由InvokeJava.cpp與InvocationTest.java兩個文件構成 示例程序將按如下順序執行: (1) 主程序InvokeJava.cpp使用Invocation API加載Java虛擬機。 (2) 通過JNI函數加載InvocationTest類至內存中 (3) 執行被加載的InvocatonTest類main()方法 *** 分析Java代碼 InvocationApiTest.java #+BEGIN_SRC java public class InvocationAPiTest { public static void main(String[] args) { System.out.println(args[0]); } } #+END_SRC =僅含有一個main()方法,該main()方法是一個靜態方法,帶有一個字符串對象數組,在方法體中僅有一條輸出語句,用來降低一個數組元素args[0]中的字符串輸出到控制檯上。= *** 分析C代碼 invocationApi.c #+BEGIN_SRC c #include <jni.h> int main() { JNIEnv *env; JavaVM *vm; JavaVMInitArgs vm_args; JavaVMOptions options[1]; jint res; jclass cls; jmethodID mid; jstring jstr; jclass stringClass; jobjectArray args; // 1. 生成Java虛擬機選項 options[0].optionString = "-Djava.class.path=." vm_args.version = 0x00010002; vm_args.options = options; vm_args.nOptions = 1; vm_args.ignoreUnrecognized = JNI_TRUE; // 2. 生成Java虛擬機 res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args); // 3. 查找並加載類 cls = (*env)->FindClass(env, "InvocationApiTest"); // 4. 獲取main()方法的ID mid = (*env)->GetStaticMethodID(env, cls, "main", ([Ljava/lang/String;)V); // 5. 生成字符串對象,用作main()方法的參數 jstr = (*env)->NewStringUTF(env, "Hello Invacation API!!"); stringClass = (*env)->NewObjectArray(env, 1, stringClass, jstr); args = (*env)->NewObjectArray(env, 1, stringClass, jstr); // 6. 調用main()方法 (*env)->CallStaticVoidMethod(env, cls, mid, args); // 7. 銷燬Java虛擬機 (*vm)->DestroyJavaVM(vm); } #+END_SRC 下面開始分析代碼的主要部分 #include命令用來將jni.h頭文件包含到本文件中。jni.h頭文件包含C代碼使用JNI必須的各種變量類型或JNI函數的定義,在本地代碼中使用JNI時,必須將此頭文件包含到本地代碼中。 #+BEGIN_SRC java // 1. 生成Java虛擬機選項 options[0].optionString = "-Djava.class.path=." vm_args.version = 0x00010002; vm_args.options = options; vm_args.nOptions = 1; vm_args.ignoreUnrecognized = JNI_TRUE; #+END_SRC 生成一些參數或選項值,這些值在加載Java虛擬機時被引用,用來設置Java虛擬機的運行環境或控制Java虛擬機的運行,如設置CLASSPATH或輸出調試信息等。 在生成Java虛擬機選項時,使用JavaVMInitArgs與JavaVMOption結構體,它們定義在jni.h頭文件中 #+BEGIN_SRC c typedef struct JavaVMInitArgs { jint version; jint nOptions; JavaVMOption *options; jboolean ignoreUnrecognized; } JavaVMInitArgs; typedef struct JavaVMOption { char *optionString; void *extraInfo; } JavaVMOption; #+END_SRC 觀察JavaVMInitArgs結構體定義代碼,可以發現JavaVMInitArgs結構體內包含JavaVMOption結構體的指針。JavaVMOption結構體包含Java虛擬機的各個參數,JavaVMInitArgs結構體用來將這些參數選項傳遞給Java虛擬機。 接下來,看一下結構體中各個成員的含義。 JavaVMInitArgs結構體的versino成員用來指定傳遞誒虛擬機的選項的變量的形式,設定在jni.h頭文件中定義的 =JNI_Version_1_2= 的值。nOptions與options用來指定JavaVMInitArgs所指的JavaVMOption結構體數組值。nOptions指定JavaVMOption結構體數組元素的個數,options用來指向JavaVMOption結構體的地址。示例中只設置了一個Java虛擬機選項,即JavaVMOption結構體數組僅有一個元素,聲明如下 #+BEGIN_SRC c JavaVMOption options[1]; #+END_SRC 爲了指定以上JavaVMOption結構體數組,需要指定JavaVMInitArgs的options與nOptions #+BEGIN_SRC c vm_args.options = options; // JavaVMOption 結構體的地址 vm_args.nOptions = 1; // JavaVMOption 結構體數組元素個數 #+END_SRC ignoreUnrecognized是JavaVMInitArgs結構體jboolean類型的成員,當Java虛擬機獨到設置錯誤的選項值時,該成員用來決定Java虛擬機是忽略錯誤後繼續執行,還是返回錯誤後終止執行。若ignoreUnrecognized被設置爲 =JNI_TRUE= ,Java虛擬機遇到錯誤選項時,忽略錯誤後繼續執行;若被設置爲 =JNI_FALSE= ,當遇到錯誤選項,Java虛擬機將錯誤返回後終止執行。 接下來分析JavaVMOption結構體,它用來指定Java虛擬機的選項值。若想創建選項值,只要向結構體的optionString成員指定一個字符串,用作Java虛擬機選項的形式。比如示例中的"-Djava.class.path=.",用來設置標準選項,即將Java虛擬機要加載的類的默認目錄設置爲當前目錄(.),其形式爲-Dproperty=value。 #+BEGIN_SRC c res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args); #+END_SRC 本行代碼是整個程序的核心部分,即C應用程序調用 =JNI_CreateJavaVM()= 函數,生成並裝載Java虛擬機。 =JNI_CreateJavaVM()= 函數的第一個參數類型爲JavaVM,它表示Java虛擬機接口,用來生成或銷燬Java虛擬機。DestroyJavaVM()是接口函數之一,該函數用來銷燬Java虛擬機。 在 =JNI_CreateJavaVM()= 函數的第二個參數env中,保存着JNI接口的指針的地址。通過env所指的JNI接口指針,可以使用各種JNI函數,即在C/C++中,通過env,可以生成Java對象,調用相應方法等。 | JNI Invocation API- =JNI_CreateJavaVM= | | |-------------------------------------+-----------------------------------------------------------------| | 形式 | jint =JNI_CreateJavaVM= (javaVM **vm, JNIEnv **env, void *vm_args) | |-------------------------------------+-----------------------------------------------------------------| | 說明 | 裝載並初始化Java虛擬機 | |-------------------------------------+-----------------------------------------------------------------| | 參數 | vm: JavaVM指針的地址 | | | env: JNI接口指針的地址 | | | =vm_args:= 傳遞給Java虛擬機的參數 | |-------------------------------------+-----------------------------------------------------------------| | 返回值 | 成功,返回0;失敗,返回負值 | 爲了加載InvocationTest類和執行方法(向main方法傳遞字符串參數"Hello"),首先調用FindClass()函數,裝載InvocationApiTest類。而後調用GetStaticMethodID()函數,獲取main()方法的ID,準備調用main()方法。 在使用CallStaticVoidMethod()函數調用main()方法之前,首先構造出傳遞給main()方法的參數。Java的main()方法的參數是String[]數組 #+BEGIN_SRC java public static void main(String[] args) #+END_SRC 示例中將"Hello Invocation API!!"字符串傳遞給main()方法。首先調用NewStringUTF()函數,將UTF-8形式的字符串,轉換成Java字符串對象String。然後調用NewObjectArray()函數,創建String對象數組,使用創建的String對象將其初始化。先創建一個含有一個元素的String[]數組,而後將"Hello Invocation API!!"字符串賦值給數組的第一個元素。 #+BEGIN_SRC c jstr = (*env)->NewStringUTF(env, "Hello Invacation API!!"); stringClass = (*env)->NewObjectArray(env, 1, stringClass, jstr); args = (*env)->NewObjectArray(env, 1, stringClass, jstr); #+END_SRC 調用JNI本地函數處理String對象的方法有些複雜。如果你對此仍迷惑不解,我們不妨將這部分代碼轉換成與其等價的Java代碼。 如下所示,首先創建包含一個元素的字符串數組,而後將"Hello Invocation API!!"字符串賦值給數組的首個元素 #+BEGIN_SRC java String[] args = new String[1]; args[0] = "Hello Invocation API!" #+END_SRC | JNI函數 NeStringUTF | | |---------------------+------------------------------------------------------| | 形式 | jstring NewStringUTF(JNIEnv *env, const char *bytes) | |---------------------+------------------------------------------------------| | 說明 | 將UTF-8形式的C字符串轉換成java.lang.String對象 | |---------------------+------------------------------------------------------| | 參數 | env: JNI接口指針 | | | bytes: 待生成String對象的C字符串的地址 | |---------------------+------------------------------------------------------| | 返回值 | 成功,返回String對象的jstring類型的引用;失敗,返回NULL | | JNI函數 NewObjectArray | | |------------------------+----------------------------------------------------------------------------------------------| | 形式 | jarray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initalElement) | |------------------------+----------------------------------------------------------------------------------------------| | 說明 | 生成由elementClass對象組成的數組。數組元素個數由length指定,initalElement參數用來初始化對象數組 | |------------------------+----------------------------------------------------------------------------------------------| | 參數 | env: JNI接口指針 | | | length: 數組元素個數 | | | elementClass:數組元素對象的類型 | | | initialElement: 數組初始化值 | |------------------------+----------------------------------------------------------------------------------------------| | 返回值 | 若成功,則返回數組引用;失敗,則返回NULL | #+BEGIN_SRC c (*env)->CallStaticVoidMethod(env, cls, mid, args); #+END_SRC =本行代碼通過CallStaticVoidMethod()函數調用InvocationApiTest類的main()方法。在上面創建的Stringp[]數組是CallStaticVoidMethod()函數的第四個參數,該參數會被傳遞給InvocationApiTest類的main()方法。當InvocationApiTest類的main()方法被調用執行時,它會向控制檯輸出args字符串數組的 args[0]元素中的字符串。= * 直接註冊JNI本地函數 Java虛擬機在運行包含本地方法的Java應用程序時,要經過以下兩個步驟。 1. 調用System.loadLibrary()方法,將包含本地方法具體實現的C/C++運行庫加載到內存中。 2. Java虛擬機檢索加載進來的庫函數符號,在其中查找與Java本地方法擁有相同簽名的JNI本地函數符號。若找到一致的,則將本地方法映射到具體的JNI本地函數。 在Android Framework這類複雜的系統下,擁有大量的包含本地方法的java類,Java虛擬機加載相應的運行庫,再逐一檢索,將各個本地方法與相應的函數映射起來,這顯然會增加運行時間,降低運行的效率。 爲此,JNI機制提供了名稱爲RegisterNatives()的JNI函數,該函數允許C/C++開發者將JNI本地函數與Java類的本地方法直接映射在一起。當不調用RegisterNative()函數時,Java虛擬機會自動檢索並將JNI本地函數與相應的Java本地方法鏈接在一起。但當開發者直接調用RegisterNatives()函數進行映射時,Java虛擬機就不必進行映射處理,這會極大提高運行速度,提升運行效率。 由於程序員直接將JNI本地函數與Java本地方法鏈接在一起,在加載運行庫時,Java虛擬機不必爲了識別JNI本地函數而將JNI本地函數的名稱與JNI支持的命名規則進行對比,即任何名稱的函數都能直接鏈接到Java本地方法上。 ** 加載本地庫時,註冊JNI本地函數 #+BEGIN_SRC java #include "jni.h" #include <stdio.h> // JNI本地函數原型 void printHelloNative(JNIEnv *env, jobject obj); void printStringNative(JNIEnv *env, jobject obj, jstring string); JNIEXPORT jint JNICALL JNI_Onoad(JavaVM *vm, void *reserved) { JNIEnv *env = NULL; JNINativeMethod nm[2]; jclass cls; jint result = -1; if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) { printf("Error"); return JNI_ERR; } cls = env->FindClass("HelloJNI"); nm[0].name = "printHello"; nm[0].signature = "()V"; nm[0].fnPtr = (void*)printHelloNative; nm[1].name = "printString"; nm[1].signature = "(Ljava/lang/String;)V"; nm[1].fnPtr = (void*)printStringNative; env->RegisterNatives(cls, nm, 2); return JNI_VERSION_1_4; } // 實現JNI本地函數 void printHelloNative(JNIEnv *env, jobject obj) { printf("Hello World!\n"); return; } void printStringNative(JNIEnv *env, jobject obj, jstring string) { const char *str = env->GetStringUTFChars(string, 0); printf("%s!\n, str); return; } #+END_SRC #+BEGIN_SRC java void printHelloNative(JNIEnv *env, jobject obj); void printStringNative(JNIEnv *env, jobject obj, jstring string); #+END_SRC 此兩行代碼用來聲明JNI本地函數原型。如前所述,在使用RegisterNatives()函數機型映射時,不需要將JNI本地函數原型與JNI命名規則進行比對,所以使用的函數名比較簡單。但函數中的兩個公共參數必須指定爲"JNIEnv *env, jobject obj"。 #+BEGIN_SRC java if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) { printf("Error"); return JNI_ERR; #+END_SRC 在 =JNI_OnLoad()= 函數中首先判斷JNI的版本,即調用GetEnv()函數,判斷Java虛擬機是否支持JNI1.4。若java虛擬機支持JNI1.4, =JNI_OnLoad()= 函數就會返回 =JNI_VERSION_1_4= ;若不支持, =JNI_OnLoad()= 函數就會返回 =JNI_ERR= ,並終止裝載庫的行爲。 當GetEnv()函數調用完畢後,JNI接口指針被保存到env變量中,在調用FindClass()、RegisterNatives()等JNI函數時,可以使用該變量。 | JNI Invocation API - GetEnv | | |-----------------------------+---------------------------------------------------| | 形式 | jint GetEnv(JavaVM *vm, void **env, jint version) | |-----------------------------+---------------------------------------------------| | 說明 | 判斷Java虛擬機是否支持version指定的JNI版本,而後將JNI接口指針設置到*env中 | |-----------------------------+---------------------------------------------------| | 參數 | vm: JavaVM接口指針的地址 | | | env: JNI接口指針地址 | | | version: JNI版本 | |-----------------------------+---------------------------------------------------| | 返回值 | 若執行成功,返回0;失敗,返回負值 | #+BEGIN_SRC java cls = env->FindClass("HelloJNI"); #+END_SRC 爲了把聲明的JNI本地函數與JNI本地函數映射在一起,本行先調用FindClass()函數加載HelloJNI類,並將類引用保存到jclass變量cls中。 #+BEGIN_SRC java nm[0].name = "printHello"; nm[0].signature = "()V"; nm[0].fnPtr = (void*)printHelloNative; nm[1].name = "printString"; nm[1].signature = "(Ljava/lang/String;)V"; nm[1].fnPtr = (void*)printStringNative #+END_SRC 該部分代碼用來將Java類的本地方法與JNI本地函數映射在一起。首先使用JNINativeMethod結構體數組,將待映射的本地方法與JNI本地函數的相關信息保存在數組中,而後調用RegisterNatives()函數進行映射。JNINativeMethod結構體定義如下 #+BEGIN_SRC c typedef struct { char *name; // 本地方法名稱 char *signature; // 本地方法簽名 void *fnPtr; // 與本地方法相對應的JNI本地函數指針 } JNINativeMethod #+END_SRC 如代碼所示,nm是JNINativeMethod結構體數組,它保存着printHello()、printString()與printHelloNative()、printStringNative()函數的鏈接信息。 保存好映射信息後,將它們傳遞給RegisterNatives()函數,最後由RegisterNatives()函數完成映射。 | JNI函數 RegisterNatives | | |-------------------------+--------------------------------------------------------------------------------------------------| | 形式 | jarray RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methdos, jint nMethods) | |-------------------------+--------------------------------------------------------------------------------------------------| | 說明 | 將clazz指定類中的本地方法與JNI本地函數鏈接在一起,鏈接信息保存在JNINativeMethod結構體數組中 | |-------------------------+------------------------------------------------------------------------------------------------------------------------------------| | 參數 | env: JNI接口指針 | | | clazz: Java類 | | | methods: 包含本地方法與JNI本地函數的鏈接信息 | | | nMethods: methods數組元素的個數 | |-------------------------+---------------------------------------------------------------------------------------------------------| | 返回值 | 若執行成功,返回數組引用;否則返回NULL | =總結一下,本節中通過JNI_OnLoad()函數將Java本地方法與JNI本地函數映射起來。= ** Android中的應用舉例 * 使用Android NDK開發 Andoird NDK ( Native Development Kit ) + 包含將C/C++源代碼編譯成本地庫的工具(編譯器、連接器等) + 提供將編譯好的本地庫插入Android包文件(.apk)中的功能 + 在生成本地庫時,Android平臺可支持的系統頭文件與庫 + NDK開發相關的文檔、示例、規範 ** 安裝Androdi NDK 網站: http://developer.android.com/sdk/ndk/index.html ** 使用Android NDK 開發步驟 =設置好NDK環境變量後,在<NDK_HOME>/apps目錄下,會看到一些NDK使用示例程序= + hello-jni: 調用本地庫,接收"Hello from JNI"字符串,並通過TextView將其輸出 + two-libs: 調用本地庫,返回兩數之和,並通過TextView輸出 + san-angeles: 調用本地OpenGL ES API, 渲染3D圖片 + hello-gl2: 調用OpenGL ES 2.0, 渲染三角形 + bitmap-plasma: 一個使用本地代碼訪問Android Bitmap對象的像素緩存區的示例程序 ** hello-jni 內容: AndroidManifest.xml default.properties /jni /res /src /tests ndk-build後: AndroidManifest.xml default.properties /jni /libs /obj /res /src /tests #+BEGIN_SRC sh hello-jni$ tree . ├── AndroidManifest.xml ├── default.properties ├── jni │ ├── Android.mk │ └── hello-jni.c ├── libs │ └── armeabi │ ├── gdbserver │ ├── gdb.setup │ └── libhello-jni.so ├── obj │ └── local │ └── armeabi │ ├── libhello-jni.so │ └── objs-debug │ └── hello-jni │ ├── hello-jni.o │ └── hello-jni.o.d ├── res │ └── values │ └── strings.xml ├── src │ └── com │ └── example │ └── hellojni │ └── HelloJni.java └── tests ├── AndroidManifest.xml ├── default.properties └── src └── com └── example └── hellojni └── HelloJniTest.java 19 directories, 15 files #+END_SRC 關鍵的文件: java層: HelloJni.java HelloJniTest.java 資源文件:strings.xml jni層: hello-jni.c Android.mk 下面主要需要分析 HelloJni.java, hello-jni.c, Android.mk 三個文件 HelloJni.java #+BEGIN_SRC java package com.example.hellojni; import android.app.Activity; import android.widget.TextView; import android.os.Bundle; public class HelloJni extends Activity { /** onCreate函數在activity第一次被創建的時候調用 */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); /* 創建一個TextView並且設置它的內容. * 文本的內容是通過本地方法獲取的 */ TextView tv = new TextView(this); tv.setText( stringFromJNI() ); // 這裏調用了本地方法 setContentView(tv); } /* 本地方法通過本地庫hello-jni實現 * 本地庫與這個應用程序已經打包在了一起 */ public native String stringFromJNI(); /* 下面是另外一個方法的聲明,這個方法沒有通過hello-jni實現 * 這是爲了說明,你可以聲明任意的本地方法,在java代碼中 * 它們的實現會在裝載的本地庫裏尋找,當你首次調用它們的時候 * 嘗試調用這個方法會引發java.lang.UnsatisfiedLinkError exception! */ public native String unimplementedStringFromJNI(); /* 下面的代碼用來在應用程序開始的時候裝載hello-jni庫 * 這個庫在安裝的時候由包管理器已經安裝好了 */ static { System.loadLibrary("hello-jni"); } } #+END_SRC hello-jni.c #+BEGIN_SRC c #include <string.h> #include <jni.h> jstring java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv *env, jobject thiz) { return (*env)->NewStringUTF(env, "Hello from JNI !"); } #+END_SRC Android.mk #+BEGIN_SRC sh LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := hello-jni LOCALSRC_FILES := hello-jni.c include $(BUILD_SHARED_LIBRARY) #+END_SRC 一個Android.mk文件首先必須定義好 =LOCAL_PATH= 變量,用於在開發樹中查找源文件。 =LOCAL_PATH= 變量在Android.mk文件的最開始被定義,若無特殊情況,一般採用如下編寫形式: #+BEGIN_SRC sh LOCAL_PATH := $(call my-dir) #+END_SRC $(call my-dir)用來保存my-dir宏函數的返回值。my-dir是一個宏函數,由編譯系統提供,用於返回包含Android.mk文件的目錄,即將Android.mk文件所在的目錄設置爲基本目錄。 一般來說,本地庫的源代碼與Android.mk文件在同一目錄下,即在 =<PROJECT_HOME>/jni= 目錄下。若將$(call my-dir)返回值保存到 =LOCAL_PATH= 變量中,即可準確指定NDK編譯的基本文件目錄。 =include $(CLEAR_VARS)= 用來初始化Android.mk文件中" =LOCAL_XXX= "即以 =LOCAL_= 開頭的變量,如 =LOCAL_MODULE= 、 =LOCAL_SRC_FILES= 等變量,但在一開始的 =LOCAL_PATH= 變量除外。由於Android編譯系統將會 =LOCAL_XXX= 變量用作全局變量,所以需要使用該命令初始化這些變量。 =LOCAL_MODULE= 變量必須被定義,以標識在Android.mk文件中描述的每個模塊,即要生成的庫的名稱。該名稱必須唯一,且不含空格,編譯系統會自動產生合適的前綴和後綴,比如設置 =LOCAL_MODULE= 變量爲ndk-exam, 編譯後綴會生成名爲libndk-exam.so的共享庫。 =LOCAL_SRC_FILES= 變量必須包含將要編譯打包進模塊中的各個源文件。這些源文件所在目錄即是 =LOCAL_PATH= 變量指定的目錄,即 =<PROJECT_HOME>/jni= 目錄。 =include $(BUILD_SHARED_LIBRARY)= 使用 =LOCAL_MODULE= 、 =LOCAL_SRC_FILES= 等變量值,創建名稱爲 =lib$(LOCAL_MODULE).so= 的共享庫。
JAVA JNI的基本總結一籮筐
JNI的基本原理
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.