本身在外面偷偷的算了下,又有將近兩個月沒更新過blog了。趁着今天有興致,來更新JNI編程的第二篇文章。在第一篇裏,大概介紹了JNI的特色、用途和優劣。而且作一個最簡單的JNI的例子,不過說實話那個例子在實際的開發中沒有太大的價值,實際開發中所須要的JNI程序要遠遠比那個複雜。因此這一篇就來介紹下如何經過JNI實現java和C++的相互通訊,來知足實際開發的須要。前端
所謂」通訊「,其實說白了無非也就是咱們所說的方法調用,在上一篇的例子裏介紹瞭如何在Java中調用本地的DLL,其實在Java代碼中,除了對本地方法標註native關鍵字和加上要加載動態連接庫以外,JNI基本上是對上層coder透明的,上層coder調用那些本地方法的時候並不知道這個方法的方法體到底是在哪裏,這個道理就像咱們用JDK所提供的API同樣。因此在Java中使用JNI仍是很簡單的,相比之下在C++中調用java,就比前者要複雜的多了。java
如今來介紹下JNI裏的數據類型。在C++裏,編譯器會很據所處的平臺來爲一些基本的數據類型來分配長度,所以也就形成了平臺不一致性,而這個問題在Java中則不存在,由於有JVM的緣故,因此Java中的基本數據類型在全部平臺下獲得的都是相同的長度,好比int的寬度永遠都是32位。基於這方面的緣由,java和c++的基本數據類型就須要實現一些mapping,保持一致性。下面的表能夠歸納:ios
Java類型 | 本地類型 | JNI中定義的別名 |
int | long | jint |
long | _int64 | jlong |
byte | signed char | jbyte |
boolean | unsigned char | jboolean |
char | unsigned short | jchar |
short | short | jshort |
float | float | jfloat |
double | double | jdouble |
Object | _jobject* | jobject |
上面的表格是我在網上搜的,放上來給你們對比一下。對於每一種映射的數據類型,JNI的設計者其實已經幫咱們取好了相應的別名以方便記憶。若是想了解一些更加細緻的信息,能夠去看一些jni.h這個頭文件,各類數據類型的定義以及別名就被定義在這個文件中。c++
瞭解了JNI中的數據類型,下面就來看此次的例子。此次咱們用Java來實現一個前端的market(如下就用Foreground代替)用CPP來實現一個後端factory(如下用backend代替)。咱們首先仍是來編寫包含本地方法的java類。編程
package com.chnic.service; import com.chnic.bean.Order; public class Business { static{ System.loadLibrary("FruitFactory"); } public Business(){ } public native double getPrice(String name); public native Order getOrder(String name, int amount); public native Order getRamdomOrder(); public native void analyzeOrder(Order order); public void notification(){ System.out.println("Got a notification."); } public static void notificationByStatic(){ System.out.println("Got a notification in a static method."); } }
這個類裏面包含4個本地方法,一個靜態初始化塊加載將要生成的dll文件。剩下的方法都是很普通的java方法,等會在backend中回調這些方法。這個類須要一個名爲Order的JavaBean。後端
package com.chnic.bean; public class Order { private String name = "Fruit"; private double price; private int amount = 30; public Order(){ } public int getAmount() { return amount; } public void setAmount(int amount) { this.amount = amount; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } }
JavaBean中,咱們爲兩個私有屬性賦值,方便後面的例子演示。到此爲止除了測試代碼以外的Java端的代碼就所有高調了,接下來進行生成.h頭文件、創建C++工程的工做,在這裏就一筆帶過,不熟悉的朋友請回頭看第一篇。在工程裏咱們新建一個名爲Foctory的C++ source file 文件,去實現那些native方法。具體的代碼以下。app
#include <iostream.h> #include <string.h> #include "com_chnic_service_Business.h" jobject getInstance(JNIEnv* env, jclass obj_class); JNIEXPORT jdouble JNICALL Java_com_chnic_service_Business_getPrice(JNIEnv* env, jobject obj, jstring name) { const char* pname = env->GetStringUTFChars(name, NULL); cout << "Before release: " << pname << endl; if (strcmp(pname, "Apple") == 0) { env->ReleaseStringUTFChars(name, pname); cout << "After release: " << pname << endl; return 1.2; } else { env->ReleaseStringUTFChars(name, pname); cout << "After release: " << pname << endl; return 2.1; } } JNIEXPORT jobject JNICALL Java_com_chnic_service_Business_getOrder(JNIEnv* env, jobject obj, jstring name, jint amount) { jclass order_class = env->FindClass("com/chnic/bean/Order"); jobject order = getInstance(env, order_class); jmethodID setName_method = env->GetMethodID(order_class, "setName", "(Ljava/lang/String;)V"); env->CallVoidMethod(order, setName_method, name); jmethodID setAmount_method = env->GetMethodID(order_class, "setAmount", "(I)V"); env->CallVoidMethod(order, setAmount_method, amount); return order; } JNIEXPORT jobject JNICALL Java_com_chnic_service_Business_getRamdomOrder(JNIEnv* env, jobject obj) { jclass business_class = env->GetObjectClass(obj); jobject business_obj = getInstance(env, business_class); jmethodID notification_method = env->GetMethodID(business_class, "notification", "()V"); env->CallVoidMethod(obj, notification_method); jclass order_class = env->FindClass("com/chnic/bean/Order"); jobject order = getInstance(env, order_class); jfieldID amount_field = env->GetFieldID(order_class, "amount", "I"); jint amount = env->GetIntField(order, amount_field); cout << "amount: " << amount << endl; return order; } JNIEXPORT void JNICALL Java_com_chnic_service_Business_analyzeOrder (JNIEnv* env, jclass cls, jobject obj) { jclass order_class = env->GetObjectClass(obj); jmethodID getName_method = env->GetMethodID(order_class, "getName", "()Ljava/lang/String;"); jstring name_str = static_cast<jstring>(env->CallObjectMethod(obj, getName_method)); const char* pname = env->GetStringUTFChars(name_str, NULL); cout << "Name in Java_com_chnic_service_Business_analyzeOrder: " << pname << endl; jmethodID notification_method_static = env->GetStaticMethodID(cls, "notificationByStatic", "()V"); env->CallStaticVoidMethod(cls, notification_method_static); } jobject getInstance(JNIEnv* env, jclass obj_class) { jmethodID construction_id = env->GetMethodID(obj_class, "<init>", "()V"); jobject obj = env->NewObject(obj_class, construction_id); return obj; }
能夠看到,在我Java中的四個本地方法在這裏所有被實現,接下來針對這四個方法來解釋下,一些JNI相關的API的使用方法。先從第一個方法講起吧:dom
1.getPrice(String name)函數
這個方法是從foreground傳遞一個類型爲string的參數到backend,而後backend判斷返回相應的價格。在cpp的代碼中,咱們用GetStringUTFChars這個方法來把傳來的jstring變成一個UTF-8編碼的char型字符串。由於jstring的實際類型是jobject,因此沒法直接比較。工具
GetStringUTFChars方法包含兩個參數,第一參數是你要處理的jstring對象,第二個參數是否須要在內存中生成一個副本對象。將jstring轉換成爲了一個const char*了以後,咱們用string.h中帶strcmp函數來比較這兩個字符串,若是傳來的字符串是「Apple」的話咱們返回1.2。反之返回2.1。在這裏還要多說一下ReleaseStringUTFChars這個函數,這個函數從字面上不難理解,就是釋放內存用的。有點像cpp裏的析構函數,只不過Sun幫咱們已經封裝好了。因爲在JVM中有GC這個東東,因此多數java coder並無寫析構的習慣,不過在JNI裏是必須的了,不然容易形成內存泄露。咱們在這裏在release以前和以後分別打出這個字符串來看一下效果。
粗略的解釋完一些API以後,咱們編寫測試代碼。
Business b = new Business(); System.out.println(b.getPrice("Apple"));
運行這段測試代碼,控制檯上打出
Before release: Apple
After release: ��
1.2
在release以前打印出來的是咱們「須要」的Apple,release以後就成了亂碼了。因爲傳遞的是Apple,因此獲得1.2。測試成功。
2. getOrder(String name, int amount)
在foreground中能夠經過這個方法讓backend返回一個你「指定」的Order。所謂「指定」,其實也就是指方法裏的兩個參數:name和amout,在cpp的代碼在中,會根據傳遞的兩個參數來構造一個Order。回到cpp的代碼裏。
是否是以爲這句代碼似曾相識?沒錯,這句代碼很像咱們java裏寫的Class.forName(className)反射的代碼。其實在這裏FindClass的做用和上面的forName是相似的。只不過在forName中要用完整的類名,可是在這裏必須用"/"來代替「.」。這個方法會返回一個jclass的對象,其實也就是咱們在Java中說的類對象。
jmethodID construction_id = env->GetMethodID(obj_class, "<init>", "()V"); jobject obj = env->NewObject(obj_class, construction_id);
拿到"類對象"了以後,按照Java RTTI的邏輯咱們接下來就要喚醒那個類對象的構造函數了。在JNI中,包括構造函數在內的全部方法都被當作Method。每一個method都有一個特定的ID,咱們經過GetMethodID這個方法就能夠拿到咱們想要的某一個java 方法的ID。GetMethodID須要傳三個參數,第一個是很顯然jclass,第二個參數是java方法名,也就是你想取的method ID的那個方法的方法名(有些繞口 ),第三個參數是方法簽名。
在這裏有必要單獨來說一講這個方法簽名,爲何要用這個東東呢?咱們知道,在Java裏方法是能夠被重載的,好比我一個類裏有public void a(int arg)和public void a(String arg)這兩個方法,在這裏用方法名來區分方法顯然就是行不通的了。方法簽名包括兩部分:參數類型和返回值類型;具體的格式:(參數1類型簽名 參數2類型簽名)返回值類型簽名。下面是java類型和年名類型的對照的一個表
Java類型 | 對應的簽名 |
boolean | Z |
byte | B |
char | C |
shrot | S |
int | I |
long | L |
float | F |
double | D |
void | V |
Object | L用/分割包的完整類名; Ljava/lang/String; |
Array | [簽名 [I [Ljava/lang/String; |
其實除了本身對照手寫以外,JDK也提供了一個很好用的生成簽名的工具javap,cmd進入控制檯到你要生成簽名的那個類的目錄下。在這裏用Order類打比方,敲入: javap -s -private Order。 全部方法簽名都會被輸出,關於javap的一些參數能夠在控制檯下面輸入 javap -help查看。(作coder的 畢竟仍是要認幾個單詞的)
囉嗦了一大堆,仍是回到咱們剛剛的getMethodID這個方法上。由於是調用構造函數,JNI規定調用構造函數的時候傳遞的方法名應該爲<init> ,經過javap查看 咱們要的那個無參的構造函數的方法籤是()V。獲得方法簽名,最後咱們調用NewObject方法來生成一個新的對象。
拿到了對象,以後咱們開始爲對象jobject填充數值,仍是首先拿到setXXX方法的Method ID,以後調用Call<Type>Method來調用java方法。這裏的<Type>所指的是方法的返回類型,咱們剛剛調用的是set方法的返回值是void,所以這裏的方法也就是CallVoidMethod,這個方法的參數除了前兩個要傳入jobject和jmethodID以外還要傳入要調用的那個方法的參數,並且要順序必須一致,這點和Java的反射如出一轍,在這裏就很少解釋。(看到這一步是否是對java 反射又有了本身新的理解?)
終於介紹完了第二個方法,下來就是測試代碼測試。
Order o = b.getOrder("Watermelom", 100); System.out.println("java: " + o.getName()); System.out.println("java: " + o.getAmount());
控制檯打出
java: Watermelom
java: 100
就此,咱們完成了第二個方法的測試。
鑑於篇幅的關係,第三個 第四個方法的解釋放到下一篇裏解釋。忽然發現這篇寫的貌似有點長了,再寫下去的話臭雞蛋和番茄就飄過來了 , 具體的代碼也會在下一篇裏上傳。