1、前言
Java本機接口(Java Native Interface (JNI))是本機編程接口,它是JDK的一部分,JNI它提供了若干的API,實現了和Java和其餘通訊(主要是C&C++),用於從Java程序調用C/C++,以及從C/C++程序調用Java代碼。html
本文旨在強化JNI的使用技巧,簡單的使用可另外參考 https://www.cnblogs.com/blogs-of-lxl/p/9268732.html 的 JNI接口實現 部分。java
2、Java層存儲JNI層動態建立的C++對象(Java調用C++)linux
1.C++層的代碼以下:c++
定義了一個食品類,裏面有獲取食品名稱和價格的方法。編程
#pragma once class CFood { private: char* name; double price; public: CFood(char* name, double price) { this->name = name; this->price = price; } ~CFood() { if(name != NULL) { free(name); name = NULL; } } const char* getName() { return this->name; } double getPrice() { return this->price; } };
2.JNI層的實現代碼:緩存
經過JNI實現對c++類的調用。多線程
(1)頭文件:test_Food.hjvm
#include <jni.h> /* Header for class test_Food */ #ifndef _Included_test_Food #define _Included_test_Food #ifdef __cplusplus extern "C" { #endif /* * Class: test_Food * Method: setFoodParam * Signature: (Ljava/lang/String;D)V */ JNIEXPORT void JNICALL Java_test_Food_setFoodParam (JNIEnv *, jobject, jstring, jdouble); /* * Class: test_Food * Method: getName * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_test_Food_getName (JNIEnv *, jobject); /* * Class: test_Food * Method: getPrice * Signature: ()D */ JNIEXPORT jdouble JNICALL Java_test_Food_getPrice (JNIEnv *, jobject); /* * Class: test_Food * Method: finalize * Signature: ()V */ JNIEXPORT void JNICALL Java_test_Food_finalize (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
(2)代碼實現:test_Food.cppide
#include "stdafx.h" #include <stdlib.h> #include "test_Food.h" #include "Food.h" // jstring轉string類型方法 char* jstring2string(JNIEnv* env, jstring jstr) { char* rtn = NULL; jclass clsstring = env->FindClass("java/lang/String"); jstring strencode = env->NewStringUTF("utf-8"); jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B"); jbyteArray barr= (jbyteArray)env->CallObjectMethod(jstr, mid, strencode); jsize alen = env->GetArrayLength(barr); jbyte* ba = env->GetByteArrayElements(barr, JNI_FALSE); if (alen > 0) { rtn = (char*)malloc(alen + 1); memcpy(rtn, ba, alen); rtn[alen] = 0; } env->ReleaseByteArrayElements(barr, ba, 0); return rtn; } // char轉jstring類型方法 jstring char2Jstring(JNIEnv* env, const char* pat) { jclass strClass = env->FindClass("Ljava/lang/String;"); jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V"); jbyteArray bytes = env->NewByteArray(strlen(pat)); env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat); jstring encoding = env->NewStringUTF("utf-8"); return (jstring)env->NewObject(strClass, ctorID, bytes, encoding); } // 給Java層CFood對象指針賦值 void setFood(JNIEnv *env, jobject thiz, const CFood* pFood) { jclass clazz = env->GetObjectClass(thiz); jfieldID fid = env->GetFieldID(clazz, "mObject", "I"); env->SetIntField(thiz, fid, (jint)pFood); } // 獲取Java層CFood對象指針 CFood* getCFood(JNIEnv *env, jobject thiz) { jclass clazz = env->GetObjectClass(thiz); //獲取thiz中對象的class對象 jfieldID fid = env->GetFieldID(clazz, "mObject", "I"); //獲取Java中的mObject字段的id(mObject是Java中存儲C++層對象的指針) jint p = env->GetIntField(thiz, fid); //獲取mObject的值 return (CFood*)p; } // JNI接口:供Java調用c++ CFood類建立食品對象 JNIEXPORT void JNICALL Java_test_Food_setFoodParam (JNIEnv *env, jobject thiz, jstring name, jdouble price) { //const char* tempName = env->GetStringUTFChars(name, 0); char* tempName = jstring2string(env, name); double tempPrice = price; CFood* pFood = new CFood(tempName, tempPrice); //根據Java層傳下來的參數建立一個食品對象 setFood(env, thiz, pFood); //將建立的食品對象指針經過JNI賦值給Java層變量 } // JNI接口:供Java層調用獲取食品名稱 JNIEXPORT jstring JNICALL Java_test_Food_getName (JNIEnv *env, jobject thiz) { CFood* pFood = getCFood(env, thiz); const char* name = pFood->getName(); return char2Jstring(env, name); } // JNI接口:供Java調用獲取食品價格 JNIEXPORT jdouble JNICALL Java_test_Food_getPrice (JNIEnv *env, jobject thiz) { CFood* pFood = getCFood(env, thiz); return pFood->getPrice(); } // JNI接口:供Java調用析構c++對象。 JNIEXPORT void JNICALL Java_test_Food_finalize (JNIEnv *env, jobject thiz) { CFood* pFood = getCFood(env, thiz); if (pFood != NULL) { delete pFood; pFood = NULL; setFood(env, thiz, pFood); } }
3.Java層爲了使用上述代碼,引入一個新的類Food,以下:函數
public class Food { static { System.loadLibrary("jniFood"); //加載JNI及c++部分代碼編譯生成的so } // 用於存儲C++層的對象指針 private int mObject; public Food(String name, double price) { setFoodParam(name, price); }
// 本地方法,在JNI實現的接口
public native void setFoodParam(String name, double price); public native String getName(); public native double getPrice(); protected native void finalize();
// 測試 public static void main(String[] args) { Food f1 = new Food("麪包", 1.99); Food f2 = new Food("牛奶", 3.99); System.out.println(String.format("食物:%s, 單價:%f", f1.getName(), f1.getPrice())); System.out.println(String.format("食物:%s, 單價:%f", f2.getName(), f2.getPrice())); }
}
其中,聲明瞭本地方法,須要注意的是建立一個int型字段用來存放C++層對象的指針。另外須要注意的是經過本地方法finalize()來析構c++對象。
3、C++中存放Java對象(C++回調Java)
首先實現單線程的回調,始終將 JNI接口參數中的 JNIEnv * 和 jobject 一塊兒傳參使用,不做保存。
1.Java層代碼:
package test1; // 內部實現一個MyPrint類 class MyPrint { public void onPrint(String text) { System.out.println(text); } } // MyFile類 public class MyFile { private MyPrint myPrint = null; static { System.loadLibrary("jniTest"); //加載JNI動態庫 } private int mObject; // MyFile類的構造函數 public MyFile() { init(); //初始化一個C++層的CMyFile對象,並將對象的地址保存到上面的mObject變量中 } // MyFile類中的onPrint方法,調用MyPrint對象的onPrint方法 public void onPrint(String text) { myPrint.onPrint(text); } // 本地保存C++傳過來的myPrint對象 public void setPrint(MyPrint myPrint) { this.myPrint = myPrint; } // 保存Java本地myPrint方法,一樣在C++的建立CMyprint對象並註冊到CMyFile對象中去 public void setMyPrint(MyPrint myPrint) { setPrint(myPrint); this.registerPrint(); } public void myPrint(String text) { this.doPrint(text); } // 本地方法,在JNI中實現 public native void init(); public native void registerPrint(MyPrint myPrint); public native void doPrint(String text); protected native void finalize(); // 測試 public static void main(String[] args) { MyFile myFile = new MyFile(); //實例化MyFile對象,主要是C++層實例化一個CMyFile對象並將對象指針保存到Java層mObject變量 MyPrint myPrint = new MyPrint(); //實例化一個Java層MyPrint對象 myFile.setMyPrint(myPrint); //保存本地MyPrint對象,另外C++層也會建立一個CMyPrint對象並註冊到CMyFile對象中去 myFile.doPrint("hello world!"); /*經過JNI接口調用C++層CMyfile對象中CMyPrint對象的onPrint方法,而後C++層中的onPrint方法又會經過傳過去的 JNIEnv* 和 jobject 來獲取Java對象,並回調Java層的onPrint方法*/ myFile.doPrint("again!"); myFile.doPrint("next!"); } }
2.JNI層實現:
C++頭文件:MyPrint.h
#include "stdafx.h" #include <jni.h> #include <stdlib.h> class CMyPrint { public: jstring char2Jstring(JNIEnv* env, const char* pat) { jclass strClass = env->FindClass("Ljava/lang/String;"); jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V"); jbyteArray bytes = env->NewByteArray(strlen(pat)); env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat); jstring encoding = env->NewStringUTF("utf-8"); return (jstring)env->NewObject(strClass, ctorID, bytes, encoding); } // 以下傳遞JNIEnv* 和 jobject參數來獲取Java對象,而後回調 void onPrint(JNIEnv *env, jobject thiz, char* text) { jclass clazz = env->GetObjectClass(thiz); jmethodID methodID = env->GetMethodID(clazz, "onPrint", "(Ljava/lang/String;)V"); jstring strText = char2Jstring(env, text); env->CallVoidMethod(thiz, methodID, strText); } };
C++頭文件:MyFile.h
#pragma once #include "MyPrint.h" class CMyFile { private: CMyPrint* pPrint; public: ~CMyFile() { if (pPrint != NULL) { delete pPrint; pPrint = NULL; } } void registerPrint(CMyPrint* pPrint) { this->pPrint = pPrint; } void doPrint(JNIEnv *env1, jobject thiz, char* text) { pPrint->onPrint(env1, thiz, text); } };
JNI頭文件:test1_MyFile.h
#include <jni.h>
/* Header for class test1_MyFile */ #ifndef _Included_test1_MyFile #define _Included_test1_MyFile #ifdef __cplusplus extern "C" { #endif /* * Class: test1_MyFile * Method: init * Signature: ()V */ JNIEXPORT void JNICALL Java_test1_MyFile_init (JNIEnv *, jobject); /* * Class: test1_MyFile * Method: registerPrint * Signature: (Ltest1/MyPrint;)V */ JNIEXPORT void JNICALL Java_test1_MyFile_registerPrint (JNIEnv *, jobject); /* * Class: test1_MyFile * Method: doPrint * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_test1_MyFile_doPrint (JNIEnv *, jobject, jstring); /* * Class: test1_MyFile * Method: finalize * Signature: ()V */ JNIEXPORT void JNICALL Java_test1_MyFile_finalize (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
JNI實現代碼:
#include "stdafx.h" #include <jni.h> #include "MyFile.h" #include "test1_MyFile.h"
// jstring轉string類型方法 char* jstring2string(JNIEnv* env, jstring jstr) { char* rtn = NULL; jclass clsstring = env->FindClass("java/lang/String"); jstring strencode = env->NewStringUTF("utf-8"); jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B"); jbyteArray barr= (jbyteArray)env->CallObjectMethod(jstr, mid, strencode); jsize alen = env->GetArrayLength(barr); jbyte* ba = env->GetByteArrayElements(barr, JNI_FALSE); if (alen > 0) { rtn = (char*)malloc(alen + 1); memcpy(rtn, ba, alen); rtn[alen] = 0; } env->ReleaseByteArrayElements(barr, ba, 0); return rtn; }
// 獲取Java層mObject保存的指針 CMyFile* getMyFile(JNIEnv *env, jobject thiz) { jclass clazz = env->GetObjectClass(thiz); jfieldID fid = env->GetFieldID(clazz, "mObject", "I"); jint p = env->GetIntField(thiz, fid); return (CMyFile*)p; }
// 將CMyFile對象指針保存到Java層的mObjecj變量中 void setMyFile(JNIEnv *env, jobject thiz, CMyFile* pFile) { jclass clazz = env->GetObjectClass(thiz); jfieldID fid = env->GetFieldID(clazz, "mObject", "I"); env->SetIntField(thiz, fid, (jint)pFile); }
// 建立C++層的CMyFile對象,並將對象指針保存到Java層 JNIEXPORT void JNICALL Java_test1_MyFile_init (JNIEnv *env, jobject thiz) { CMyFile* pFile = new CMyFile(); setMyFile(env, thiz, pFile); }
// 建立C++層的CMyPrint對象,並註冊到CMyFile對象中 JNIEXPORT void JNICALL Java_test1_MyFile_registerPrint (JNIEnv *env, jobject thiz) { CMyPrint* pPrint = new CMyPrint(); CMyFile* pFile = getMyFile(env, thiz); pFile->registerPrint(pPrint); }
// 調用CMyFile對象中註冊的CMyPrint對象的打印方法 JNIEXPORT void JNICALL Java_test1_MyFile_doPrint (JNIEnv *env, jobject thiz, jstring strText) { CMyFile* pFile = getMyFile(env, thiz); char* pText = jstring2string(env, strText); pFile->doPrint(env, thiz, pText); if (pText != NULL) { free(pText); pText = NULL; } }
// 析構C++層的CMyFile對象 JNIEXPORT void JNICALL Java_test1_MyFile_finalize (JNIEnv *env, jobject thiz) { CMyFile* pFile = getMyFile(env, thiz); if (pFile != NULL) { delete pFile; pFile = NULL; setMyFile(env, thiz, pFile); } }
上述的回調是在一個線程棧中完成的,那如何實現多線程的回調實現呢?因爲JNIEnv *不能被緩存,只在當前線程中有效,並且JNI中接口的參數都是局部引用,當該方法棧執行完畢,局部引用就會被銷燬,因此每次都要獲取JNIEnv *和jobject對象參數進行回調,而能夠緩存的是JavaVM*,一樣應該將JavaVM*轉換爲全局引用再緩存,jobject也能夠轉換爲全局引用後緩存。
繼續在上面代碼進行修改,共同部分就不貼了:
1. Java層代碼:
package test1; class MyPrint { public void onPrint(String text) { System.out.println(text); } } public class MyFile { private MyPrint myPrint = null; static { System.loadLibrary("jniTest"); } private int mObject; public MyFile() { init(); //初始化一個C++層的CMyFile對象,並將對象的地址保存到上面的mObject變量中 } public void setPrint(MyPrint myPrint) { this.myPrint = myPrint; } public void setMyPrint(MyPrint myPrint) { setPrint(myPrint); this.registerPrint(myPrint); // 保存Java本地myPrint方法,且建立CMyPrint對象保存 JavaVM* , jobject 參數 }
// 本地方法 public native void init(); protected native void finalize(); public native void registerPrint(MyPrint myPrint); public native void doPrint(String text);
// 測試 public static void main(String[] args) { MyFile myFile = new MyFile(); MyPrint myPrint = new MyPrint(); myFile.setMyPrint(myPrint); myFile.doPrint("hello world!"); System.out.println("等待打印結果..."); try { Thread.sleep(20*1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
2.JNI層實現:
CMyFile* getMyFile(JNIEnv *env, jobject thiz) { jclass clazz = env->GetObjectClass(thiz); jfieldID fid = env->GetFieldID(clazz, "mObject", "I"); jint p = env->GetIntField(thiz, fid); return (CMyFile*)p; } void setMyFile(JNIEnv *env, jobject thiz, CMyFile* pFile) { jclass clazz = env->GetObjectClass(thiz); jfieldID fid = env->GetFieldID(clazz, "mObject", "I"); env->SetIntField(thiz, fid, (jint)pFile); } JNIEXPORT void JNICALL Java_test1_MyFile_init (JNIEnv *env, jobject thiz) { CMyFile* pFile = new CMyFile(); setMyFile(env, thiz, pFile); } JNIEXPORT void JNICALL Java_test1_MyFile_finalize (JNIEnv *env, jobject thiz) { CMyFile* pFile = getMyFile(env, thiz); if (pFile != NULL) { delete pFile; pFile = NULL; setMyFile(env, thiz, pFile); } } JNIEXPORT void JNICALL Java_test1_MyFile_registerPrint (JNIEnv *env, jobject thiz, jobject jPrint) { JavaVM* pVM = NULL; env->GetJavaVM(&pVM); // 根據局部引用生成全局引用 JavaVM* g_pVM = (JavaVM*)env->NewGlobalRef((jobject)pVM); jobject g_javaPrint = env->NewGlobalRef(jPrint); CMyPrint* pPrint = new CMyPrint(g_pVM, g_javaPrint); // 其中會保存 g_pVM 和 g_javaPrint CMyFile* pFile = getMyFile(env, thiz); pFile->registerPrint(pPrint); } JNIEXPORT void JNICALL Java_test1_MyFile_doPrint (JNIEnv *env, jobject thiz, jstring strText) { CMyFile* pFile = getMyFile(env, thiz); char* pText = CMyPrint::jstring2string(env, strText); pFile->doPrint(pText); if (pText != NULL) { free(pText); pText = NULL; } }
3.C++層代碼:
typedef struct _ThreadParam { JavaVM* jvm; jobject javaPrint; string text; }ThreadParam; DWORD WINAPI funproc(LPVOID lpparentet) { Sleep(10*1000); ThreadParam* param = (ThreadParam*)lpparentet; JNIEnv* pEnv = NULL; param->jvm->AttachCurrentThread((void**)&pEnv, NULL); //附加當前線程到Java(Dalvik)虛擬機(建立CMyPrint時保存了jvm) jclass clazz = pEnv->GetObjectClass(param->javaPrint); // 獲取非靜態方法ID jmethodID methodID = pEnv->GetMethodID(clazz, "onPrint", "(Ljava/lang/String;)V"); jstring strText = CMyPrint::char2Jstring(pEnv, param->text.c_str()); // 調用非靜態方法 pEnv->CallVoidMethod(param->javaPrint, methodID, strText); if (param != NULL) { delete param; param = NULL; } return 0; } /* * CMyPrint 類 */ class CMyPrint { private: jobject mJavaPrintObj; JavaVM* jvm; public: CMyPrint(JavaVM* jvm, jobject javaPrintObj) // 建立CMyPrint對象時保存JavaVM*和jobject,可用於子線程回調 { this->jvm = jvm; this->mJavaPrintObj = javaPrintObj; } ~CMyPrint() { JNIEnv* pEnv = NULL; jvm->AttachCurrentThread((void**)&pEnv, NULL); pEnv->DeleteGlobalRef(mJavaPrintObj); pEnv->DeleteGlobalRef((jobject)jvm); } static char* jstring2string(JNIEnv* env, jstring jstr) { char* rtn = NULL; jclass clsstring = env->FindClass("java/lang/String"); jstring strencode = env->NewStringUTF("utf-8"); jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B"); jbyteArray barr= (jbyteArray)env->CallObjectMethod(jstr, mid, strencode); jsize alen = env->GetArrayLength(barr); jbyte* ba = env->GetByteArrayElements(barr, JNI_FALSE); if (alen > 0) { rtn = (char*)malloc(alen + 1); memcpy(rtn, ba, alen); rtn[alen] = 0; } env->ReleaseByteArrayElements(barr, ba, 0); return rtn; } static jstring char2Jstring(JNIEnv* env, const char* pat) { jclass strClass = env->FindClass("Ljava/lang/String;"); jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V"); jbyteArray bytes = env->NewByteArray(strlen(pat)); env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat); jstring encoding = env->NewStringUTF("utf-8"); return (jstring)env->NewObject(strClass, ctorID, bytes, encoding); } void onPrint(char* text) { ThreadParam* param = new ThreadParam(); param->jvm = jvm; param->javaPrint = mJavaPrintObj; param->text = text; HANDLE hander = CreateThread(NULL,0,funproc,param,0,NULL); // 建立一個子線程回調Java層的方法 } }; /* * CMyFile 類 */ class CMyFile { private: CMyPrint* pPrint; public: ~CMyFile() { if (pPrint != NULL) { delete pPrint; pPrint = NULL; } } void registerPrint(CMyPrint* pPrint) { this->pPrint = pPrint; } void doPrint(char* text) { pPrint->onPrint(text); } };
四、JNI中的字符編碼方式的完美轉換
一、相關概念:
(1)、Java層使用的是16bit的unicode編碼(utf-16)來表示字符串,不管中文仍是英文,都是兩個字節。
(2)、JNI層使用的是UTF-8編碼,UTF-8是變長編碼的unicode,通常ascii字符1字節,中文3字節。
(3)、C/C++使用的是原始數據,ascii就是一個字節,中文通常是GB2312編碼,用兩個字節表示一個漢字。
二、字符流向
(1)、Java ---> C/C++ :
這時候,Java調用的時候使用的是UTF-16編碼,當字符串傳遞給JNI方法時,C/C++獲得的輸入是jstring,這時候,JNI提供了兩個函數,一個是GetStringUTFChars,該函數將獲得一個UTF-8編碼的字符串(char*類型),另外一個函數是GetStringChars,該函數將獲得一個UTF-16編碼的字符串(wchar_t*類型)。不管哪一種結果,獲得的字符串若是含有中文,都須要進一步轉換爲GB2312編碼。
(2)、C/C++ ---> Java :
這時候,是JNI返回給Java字符串。C/C++首先應該負責把這個字符串變成UTF-8或UTF-16格式,而後經過NewStringUTF或者NewString來把它封裝成jstring,返回給Java就能夠了。
若是字符串中不含中文字符,只是標準的ascii碼,那麼使用GetStringUTFChars/NewStringUTF就能夠搞定了,由於這種狀況下,UTF-8編碼和ascii編碼是一致的,不須要轉換。
若是字符串中有中文字符,那麼在C/C++部分就必須進行編碼轉換。咱們須要兩個轉換函數,一個是把UTf-8/-16編碼轉成GB2312;另外一個是把GB2312轉成UTF-8/-16。
這裏須要說明一下:linux和win32都支持wchar,這個事實上就是寬度爲16bit的unicode編碼UTF-16,因此,若是咱們的c/c++程序中徹底使用wchar類型,那麼理論上就不須要這種轉換。可是實際上,咱們不可能徹底用wchar來取代char的,因此就目前大多數應用而言,轉換仍然是必須的。
(3)、使用wide char類型來轉換 :
char* jstringToWindows( JNIEnv *env, jstring jstr ) { //UTF8/16轉換成gb2312 int length = (env)->GetStringLength(jstr ); const jchar* jcstr = (env)->GetStringChars(jstr, 0 ); int clen = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)jcstr, length, NULL,0, NULL, NULL ); char* rtn = (char*)malloc( clen ) //更正。做者原來用的是(char*)malloc( length*2+1 ),當java字符串中同時包含漢字和英文字母時,所需緩衝區大小並非2倍關係。 int size = 0; size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)jcstr, length, rtn,clen, NULL, NULL ); if( size <= 0 ) return NULL; (env)->ReleaseStringChars(jstr, jcstr ); rtn[size] = 0; return rtn; } jstring WindowsTojstring( JNIEnv* env, const char* str ) {//gb2312轉換成utf8/16 jstring rtn = 0; int slen = strlen(str); unsigned short * buffer = 0; if( slen == 0 ) rtn = (env)->NewStringUTF(str ); else { int length = MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, NULL, 0 ); buffer = (unsigned short *)malloc( length*2 + 1 ); if( MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, (LPWSTR)buffer, length ) >0 ) rtn = (env)->NewString( (jchar*)buffer, length ); } if( buffer ) free( buffer ); return rtn; }