標籤:
概述
本文主要Java與C++之間的對象傳遞與取值。包括傳遞Java對象、返回Java對象、修改Java對象、以及效能對比。
通過JNIEnv完成資料轉換
Java對象是存在於JVM虛擬機器中的,而C++是脫離JVM而啟動並執行,如果在C++中訪問和使用Java中的對象,必然會使用JNIEnv這個橋樑。其實通過下面的代碼很容易看出,這種訪問方式和Java中的反射十分雷同。
這裡定義一個簡單Java對象用於下文測試:
package com.example.wastrel.hellojni;/** * Created by wastrel on 2016/8/24. */public class Bean { private String msg; private int what; public Bean(String msg,int what) { this.msg=msg; this.what=what; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public int getWhat() { return what; } public void setWhat(int what) { this.what = what; } @Override public String toString() { return "Msg:"+msg+";What:"+what; }}
從C++中建立一個Java對象並返回
//Java中的native方法聲明 public native Bean newBean(String msg,int what);
//C++中的方法實現JNIEXPORT jobject JNICALL Java_com_example_wastrel_hellojni_HelloJNI_newBean (JNIEnv *env, jobject obj, jstring msg,jint what){ //先找到class jclass bean_clz=env->FindClass("com/example/wastrel/hellojni/Bean"); //在實際應用中應該確保你的class、method、field存在。減少此類判斷。 if(bean_clz==NULL) { LOGE("can‘t find class"); return NULL; } //擷取建構函式。建構函式的傳回值是void,因此這裡方法簽名最後為V jmethodID bean_init=env->GetMethodID(bean_clz,"<init>","(Ljava/lang/String;I)V"); if(bean_init==NULL) { LOGE("can‘t find init function"); return NULL; } //然後調用建構函式獲得bean jobject bean=env->NewObject(bean_clz,bean_init,msg,what); return bean;}
註:如果提示找不到NULL 請include<stddef.h>
C++中解析Java對象
//java方法Native聲明public native String getString(Bean bean);
//C++中的方法實現JNIEXPORT jstring JNICALL Java_com_example_wastrel_hellojni_HelloJNI_getString (JNIEnv *env, jobject obj,jobject bean){ jclass bean_clz=env->FindClass("com/example/wastrel/hellojni/Bean");//這部分是通過get函數去擷取對應的值 // jmethodID bean_getmsg=env->GetMethodID(bean_clz,"getMsg","()Ljava/lang/String;");// jmethodID bean_getwhat=env->GetMethodID(bean_clz,"getWhat","()I");// jstring jmsg=(jstring)env->CallObjectMethod(bean,bean_getmsg);// jint what=env->CallIntMethod(bean,bean_getwhat);//這部分是通過類的成員變數直接取擷取值,你可能注意到在Java中定義的變數都是private修飾的,但在反射的調用下是毫無作用的。 jfieldID bean_fmsg=env->GetFieldID(bean_clz,"msg","Ljava/lang/String;"); jfieldID bean_fwhat=env->GetFieldID(bean_clz,"what","I"); jstring jmsg=(jstring)env->GetObjectField(bean,bean_fmsg); jint what=env->GetIntField(bean,bean_fwhat);//將拿到的值拼裝一個String返回回去 const char * msg=env->GetStringUTFChars(jmsg,NULL); char *str=new char[1024]; sprintf(str,"Msg:%s;What:%d(From C++)",msg,what); jstring rs=env->NewStringUTF(str); delete []str; env->ReleaseStringUTFChars(jmsg,msg); return rs;}
註:sprintf函數包含在stdio.h標頭檔中
C++中修改Java對象屬性值
//java方法Native聲明public native void ModifyBean(Bean bean);
//C++實現JNIEXPORT void JNICALL Java_com_example_wastrel_hellojni_HelloJNI_ModifyBean (JNIEnv *env, jobject obj,jobject bean){ jclass bean_clz=env->FindClass("com/example/wastrel/hellojni/Bean"); jfieldID bean_fmsg=env->GetFieldID(bean_clz,"msg","Ljava/lang/String;"); jfieldID bean_fwhat=env->GetFieldID(bean_clz,"what","I"); jstring msg=env->NewStringUTF("Modify in C++"); //重新設定屬性 env->SetObjectField(bean,bean_fmsg,msg); env->SetIntField(bean,bean_fwhat,20); return;}
結果圖
//java中調用代碼 HelloJNI helloJNI=new HelloJNI(); Bean bean=helloJNI.newBean("This is from C++ bean",10); tv.setText(bean.toString()); bean=new Bean("This is from Java bean",15); tv.append("\n"+helloJNI.getString(bean)); helloJNI.ModifyBean(bean); tv.append("\n"+bean.toString());
Java中new Object和C++中new Object的效能對比
下面我們通過一個測試函數來比較通過兩種方式的效能,這裡可以毫無疑問的告訴你,Java一定比C++的快。那麼這個對比的意義就在於,使用C++建立Java對象的時候會不會造成不可接受的卡頓。
這裡使用的測試機是華為Mate7,具體硬體設定可自行百度。
測試函數如下:
void Test(int count) { long startTime=System.currentTimeMillis(); for (int i=0;i<count;i++) { new Bean("123",i); } long endTime=System.currentTimeMillis(); Log.e("Java","Java new "+count+"s waste "+(endTime-startTime)+"ms"); HelloJNI helloJNI=new HelloJNI(); startTime=System.currentTimeMillis(); for (int i=0;i<count;i++) { helloJNI.newBean("123",i); } endTime=System.currentTimeMillis(); Log.e("C++","C++ new "+count+"s waste "+(endTime-startTime)+"ms"); }
測試結果:
Java: Java new 5000s waste 3msC++: C++ new 5000s waste 38msJava: Java new 10000s waste 6msC++: C++ new 10000s waste 79msJava: Java new 50000s waste 56msC++: C++ new 50000s waste 338msJava: Java new 100000s waste 60msC++: C++ new 100000s waste 687ms
通過結果可以看出,通過C++來new對象比Java慢了足足10倍左右。但是從時間上來講,如果只是在C++中new一個Java對象,幾個微秒的時間差距完全是可以忽略不計的。
也許有人就會說,C++慢那麼多是因為每次都在FindClass,GetMethodId,而在程式運行過程中這兩個值是不會改變的。聽起來確實有這樣一個原因,下面我們將C++中的代碼稍作修改緩衝jclass和jmethodId。
修改後的newBean函數:
//用靜態變數緩衝static jclass bean_clz=NULL;static jmethodID bean_init=NULL;JNIEXPORT jobject JNICALL Java_com_example_wastrel_hellojni_HelloJNI_newBean (JNIEnv *env, jobject obj, jstring str,jint what){ //先找到class if(bean_clz==NULL) { jclass _bean_clz=env->FindClass("com/example/wastrel/hellojni/Bean"); bean_clz=(jclass)env->NewGlobalRef(_bean_clz); } //擷取建構函式。建構函式的傳回值是void,因此這裡方法簽名最後為V if(bean_init==NULL) { bean_init=env->GetMethodID(bean_clz,"<init>","(Ljava/lang/String;I)V"); } //然後調用建構函式獲得bean jobject bean=env->NewObject(bean_clz,bean_init,str,what); return bean;}
你可能發現了緩衝方法ID和緩衝jclass似乎不一樣。那是因為jclass其實是java.lang.Class對象,而方法ID是JNI中定義的一個結構體。如果這裡不使用env—>NewGlobalRef()函式宣告其是一個全域引用的話,在啟動並執行時候可能就會報錯:JNI ERROR (app bug): accessed stale local reference 0x5900021;表明在Jvm中該對象已經被回收了,引用已經失效了。而NewGlobalRef的作用就在於告訴JVM,C++中一直持有該引用,請不要回收。顯然這又引發了另外一個問題,你需要在你不需要該引用的時候告訴JVM,那麼就需要調用env->DelGlobalRef()。當然你也可以不調用,那麼該Java對象將在你的程式關閉的時候被回收。
測試結果:
Java: Java new 5000s waste 3msC++: C++ new 5000s waste 18msJava: Java new 10000s waste 5msC++: C++ new 10000s waste 24msJava: Java new 50000s waste 44msC++: C++ new 50000s waste 121msJava: Java new 100000s waste 65msC++: C++ new 100000s waste 259ms
這次的結果表明,如果緩衝方法ID和jclass能縮短一半的時間。但仍然不如Java快。這也很好理解,C++建立Java對象最終還是通過Java建立的,反覆的通過反射去建立自然不如自身建立來得快。
總結
- JNI中想訪問Java Object方法簽名、類名和變數名十分重要,一旦確定了就不要輕易單方面修改Java中的定義。因為這會導致JNI找不到相關的方法或類等,而引發JNI錯誤。
- 雖然JNI提供了各種方法來完成Java的反射操作,但是請酌情使用,因為這會讓Java代碼與C++代碼之間過度依賴。
- 當你需要返回C++中的結構體資料的時候,可以考慮把結構體轉換成對應的Java對象返回。
Android Studio NDK 入門教程(5)--Java對象的傳遞與修改