背景
隨着Android項目中c++代碼部分功能複雜程度的增加,jni中需要傳遞的數據類型也越來越多,關於jni數據類型轉換網上有不少相關文章,但是在使用時發現這些例子中存在不少謬誤,遂在此重新總結相關內容,並附相關例程,以便日後參考。
下文我們將對以下幾種常見情況進行分析,其中前4種是java向native傳遞數據,後3種是native向java返回數據,分別列舉如下:
- java向native傳遞常用基本數據類型 和字符串類型
- java向native傳遞數組類型
- java向native傳遞自定義java對象
- java向native傳遞任意java對象(以向native傳遞ArrayList爲例)
- native向java傳遞數組類型
- native向java傳遞字符串類型
- native向java傳遞java對象
例程
此處先介紹一下後面例子中使用的jni包裝類,該類提供了上述7種常用方法的java封裝,代碼如下
/**
* Created by lidechen on 1/23/17.
*/
public class JNIWrapper {
// java向native傳遞常用基本數據類型 和字符串類型
public native void setInt(int data);
public native void setLong(long data);
public native void setFloat(float data);
public native void setDouble(double data);
public native void setString(String data);
//java向native傳遞任意java對象(以向native傳遞ArrayList爲例)
public native void setList(List list, int len);
//java向native傳遞自定義java對象
public native void setClass(Package data);
//java向native傳遞數組類型
public native void setBuf(byte[] buf, int len);
//native向java傳遞字符串類型
public native String getString();
//native向java傳遞數組類型
public native byte[] getBuf();
//native向java傳遞java對象
public native Package getPackage();
public static class Package{
public boolean booleanData;
public byte byteData;
private int intData;
public long longData;
public float floatData;
public double doubleData;
public String stringData;
public byte[] byteArray;
public List<String> list;
public void setIntData(int data){
intData = data;
}
public int getIntData(){
return intData;
}
}
}
1.java向native傳遞常用基本數據類型 和字符串類型
java層
//傳遞常用基本類型
wrapper.setInt(123);
wrapper.setLong(123L);
wrapper.setFloat(0.618f);
wrapper.setDouble(0.618);
wrapper.setString("hello");
對應native層
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setInt
* Signature: (I)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setInt
(JNIEnv *env, jobject obj , jint data){
LOGE("setInt %d", data);
}
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setLong
* Signature: (J)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setLong
(JNIEnv *env, jobject obj, jlong data){
LOGE("setLong %ld", data);
}
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setFloat
* Signature: (F)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setFloat
(JNIEnv *env, jobject obj, jfloat data){
LOGE("setLong %f", data);
}
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setDouble
* Signature: (D)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setDouble
(JNIEnv *env, jobject obj, jdouble data){
LOGE("setDouble %lf", data);
}
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setString
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setString
(JNIEnv *env, jobject obj, jstring jdata){
const char* cdata = env->GetStringUTFChars(jdata, 0);
LOGE("setString %s", cdata);
env->ReleaseStringUTFChars(jdata, cdata);
env->DeleteLocalRef(jdata);
}
上述代碼中,除了引用類型String外,其他基本類型是直接可以拿來就用的,而且也不需要進行額外的回收操作。可以看一下jni.h中對於基本類型的定義:
jni.h中定義
#ifdef HAVE_INTTYPES_H
# include <inttypes.h> /* C99 */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#else
typedef unsigned char jboolean; /* unsigned 8 bits */
typedef signed char jbyte; /* signed 8 bits */
typedef unsigned short jchar; /* unsigned 16 bits */
typedef short jshort; /* signed 16 bits */
typedef int jint; /* signed 32 bits */
typedef long long jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#endif
可見, 這些j開頭的基本類型其實和c的基本類型是等價的,我們直接使用即可。
需要注意的是String類型,它是一個java的引用類型,這裏我們需要使用env的方法GetStringUTFChars將java的String類型轉換成c++中的const char*類型,相當於是一個字符串常量,我們只能讀取這個字符串的信息。另外在使用完畢後我們需要釋放這個String類型對象的資源,這點不同於基本類型可以直接不管。
2. java向native傳遞數組類型
java向native傳遞數組類型比較常見,比如我們經常會在java中獲取一些音頻或者圖像數據,然後傳遞到native層,使用c++編寫的算法對這些數據進行處理,而且這類代碼往往傳送的數據量比較大,如果沒有正確釋放很可能會吃盡所有系統內存。
這裏也要順便說明一點,我們在native中開闢的堆內存是不受android虛擬機內存限制的,可以通過在jni中malloc一塊大於當前虛擬機限制的內存來驗證。
下面是java層向native層傳入數組的例子
//傳遞數組
byte[] buf = new byte[5];
buf[0] = 49;
buf[1] = 50;
buf[2] = 51;
buf[3] = 52;
buf[4] = 53;
wrapper.setBuf(buf, 5);
native層接收數據
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setBuf
* Signature: ([B)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setBuf
(JNIEnv *env, jobject obj, jbyteArray jbuf, jint len){
jbyte *cbuf = env->GetByteArrayElements(jbuf, JNI_FALSE);
for(int i=0; i<len; i++){
LOGE("setBuf buf[%d] %c", i, cbuf[i]);
}
env->ReleaseByteArrayElements(jbuf, cbuf, 0);
env->DeleteLocalRef(jbuf);
}
這裏還是調用方法先將java的數組轉換爲c的指針類型,這樣就能拿到數組中的元素的了。與上述String一樣,使用完數組後我們必須將其釋放。
可見,java傳入一般類型的數據,到了native層首先是要將其轉換成c中對應的類型,然後使用c的方式對數據進行操作,最後再釋放資源。
上述的幾種類型jni.h中已經給我們提供了相應的轉換函數以及對應的轉換類型,但是對於自定義的數據類型或者java自帶的其它類型如何傳遞給native層呢?
3. java向native傳遞自定義java對象
傳遞自定義的java對象在開發中是很常見的,比如一個算法需要接受很多個參數,如果直接寫到函數jni的參數中,那麼如果還要增加或者減少參數數量或者改變類型時必然需要重新生成jni接口。這時我會將這些參數封裝爲一個類,定義一個對象將其一起傳遞給native層。
這裏我們傳遞自定義的類型Package,定義在JNIWrapper中。
java層構造並傳入對象
//傳遞自定義java對象 並在jni中獲取java對象的屬性值
JNIWrapper.Package pkg = new JNIWrapper.Package();
pkg.booleanData = true;
//pkg.intData = 12345;
//注意 int 參數是一個私有屬性,在jni中也可以直接拿到
pkg.setIntData(12345);
pkg.longData = 12345L;
pkg.floatData = 3.14159f;
pkg.doubleData = 3.14159;
pkg.stringData = "hello class";
pkg.byteArray = buf;
List<String> list2 = new ArrayList<String>();
list2.add("str 1");
list2.add("str 2");
list2.add("str 3");
pkg.list = list2;
wrapper.setClass(pkg);
native層接收對象
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setClass
* Signature: (Lcom/vonchenchen/myapplication/JNIWrapper/Package;)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setClass
(JNIEnv *env, jobject obj, jobject data){
jclass cdata = env->GetObjectClass(data);
//boolean 比較特殊
jfieldID booleanDataID = env->GetFieldID(cdata, "booleanData", "Z");
jboolean cbooleanData = env->GetBooleanField(data, booleanDataID);
jfieldID byteDataID = env->GetFieldID(cdata, "byteData", "B");
jboolean cbyteData = env->GetByteField(data, byteDataID);
//注意JAVA 對象的私有屬性此處也可以獲取到
jfieldID intDataID = env->GetFieldID(cdata, "intData", "I");
jint cintData = env->GetIntField(data, intDataID);
//long比較特殊
jfieldID longDataID = env->GetFieldID(cdata, "longData", "J");
jlong clongData = env->GetLongField(data, longDataID);
jfieldID floatDataID = env->GetFieldID(cdata, "floatData", "F");
jfloat cfloatData = env->GetFloatField(data, floatDataID);
//
jfieldID doubleDataID = env->GetFieldID(cdata, "doubleData", "D");
jdouble cdoubleData = env->GetDoubleField(data, doubleDataID);
jfieldID stringDataID = env->GetFieldID(cdata, "stringData", "Ljava/lang/String;");
jstring cstringData = (jstring)env->GetObjectField(data, stringDataID);
const char *ccharData = env->GetStringUTFChars(cstringData, JNI_FALSE);
//
LOGE("setClass bool %d", cbooleanData);
LOGE("setClass byte %d", cbyteData);
LOGE("setClass int %d", cintData);
LOGE("setClass long %ld", clongData);
LOGE("setClass float %f", cfloatData);
LOGE("setClass double %lf", cdoubleData);
LOGE("setClass String %s", ccharData);
env->ReleaseStringUTFChars(cstringData, ccharData);
env->DeleteLocalRef(cstringData);
}
jni並不知道我們傳入數據的類型,也就沒辦法拿到這個對象的屬性或者操作這個對象的方法。所以第一步先是通過調用GetObjectClass方法,得到我們傳入對象的類。
有了這個類之後,我們就可以用GetxxxField方法和GetMethodID方法分別拿到對象的屬性和方法ID。 有了方法或者屬性id,我們就可以從傳入的jobject對象中獲取這個屬性或者方法並執行了。
注意,我們在獲取屬性時需要知道這個屬性的簽名,那麼簽名如何拿到呢?
如何獲取屬性和方法的簽名
現在以我們的Package爲例,來看一下獲取簽名的流程。
這裏我們使用javap命令,來對要查看類的.class文件進行操作。那麼我們就先找到.class文件的存放位置。下圖是Android Studio生成.class的文件位置。
這裏找到了JNIWrapper的class文件,而Package是JNIWrapper的內部類。執行如下命令
上圖注意執行命令的目錄層級,應該在debug文件目錄下執行指令,具體路徑見截圖,大家需要根據自己的工程進行設置。
javap -s 完整類名
如果需要哪個類型的函數或者屬性簽名,直接找對應項的descriptor即可。
4. java向native傳遞任意java對象(以向native傳遞ArrayList爲例)
現在我們把ArrayList傳給native層,讓native層拿到ArrayList中存儲的數據。
java層傳遞ArrayList給native層
//傳入一般類型的java對象 並在jni中調用java方法 此處以ArrayList爲例
List<Integer> list = new ArrayList<Integer>();
list.add(111);
list.add(222);
list.add(333);
wrapper.setList(list, 3);
native層接收
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: setList
* Signature: (Ljava/util/List;)V
*/
JNIEXPORT void JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_setList
(JNIEnv *env, jobject obj, jobject data, jint len){
//傳入一個JAVA 的ArrayList對象,存放的範型爲Integer,下面我們嘗試拿到ArrayList的第一個元素
//獲取傳入對象的java類型,也就是ArrayList
jclass datalistcls = env->GetObjectClass(data);
//執行 javap -s java.util.ArrayList 查看ArrayList的函數簽名
/* public E get(int);
descriptor: (I)Ljava/lang/Object;
*/
//從ArrayList對象中拿到其get方法的方法ID
jmethodID getMethodID = env->GetMethodID(datalistcls, "get", "(I)Ljava/lang/Object;");
//調用get方法,拿到list中存儲的第一個Integer 對象
jobject data0 = env->CallObjectMethod(data, getMethodID, 0);
//javap -s java/lang/Integer
jclass datacls = env->GetObjectClass(data0);
/*
* public int intValue();
descriptor: ()I
*/
jmethodID intValueMethodID = env->GetMethodID(datacls, "intValue", "()I");
//將Integer 對象的int值取出
int data0_int = env->CallIntMethod(data0, intValueMethodID);
LOGE("setList buf[0] %d", data0_int);
}
這裏先拿到傳入jobject的真正的類,然後調用get方法拿到了ArrayList中存儲的範型元素,由於我們傳入的是Integer 類型,我們在從Integer中拿到包裝的int數據,具體過程參考註釋。
5. native向java傳遞數組類型
現在開始我們來看native如何返給java數據。現在我們在native中生成一個c數組,我們將數組數據返回給java。
native方法
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: getBuf
* Signature: ()[B
*/
JNIEXPORT jbyteArray JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_getBuf
(JNIEnv *env, jobject obj){
//char *buf = "I am from jni";
char buf[] = "getBuf : I am from jni";
int len = sizeof(buf);
LOGE("sizeof %d", len); // 注意sizeof對於數組和指針是區別對待的
jbyteArray ret = env->NewByteArray(len);
env->SetByteArrayRegion(ret, 0, len, (jbyte *) buf);
return ret;
}
這裏首先創建並初始化了一個c數組,然後在native層調用NewByteArray方法生成一個java數組,再使用SetByteArrayRegion將c數組的值複製到java數組中。
6. native向java傳遞字符串類型
native 層
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: getString
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_getString
(JNIEnv *env, jobject obj){
const char buf[] = "getString : I am from jni";
return env->NewStringUTF(buf);
}
調用NewStringUTF生成一個java的string,然後返回即可。
7. native向java傳遞java對象
這裏我們可以直接在native中生成一個java對象,並且對其屬性賦值,最終將構造好的對象直接返回給java層。
native 代碼
/*
* Class: com_vonchenchen_myapplication_JNIWrapper
* Method: getPackage
* Signature: ()Lcom/vonchenchen/myapplication/JNIWrapper/Package;
*/
JNIEXPORT jobject JNICALL Java_com_vonchenchen_myapplication_JNIWrapper_getPackage
(JNIEnv *env, jobject obj){
//獲取類對象 這個class文件存在於dex中,我們可以通過分析apk工具查看
jclass packagecls = env->FindClass("com/vonchenchen/myapplication/JNIWrapper$Package");
//獲取這個類的構造方法的方法id 以及這個方法的函數簽名
jmethodID construcMethodID = env->GetMethodID(packagecls, "<init>", "()V");
//創建這個java對象
jobject packageobj = env->NewObject(packagecls, construcMethodID);
//操作對象的屬性
jfieldID intDataID = env->GetFieldID(packagecls, "intData", "I");
env->SetIntField(packageobj, intDataID, 88888);
return packageobj;
}
這裏需要注意的是使用FindClass方法找到要生成的類。Package爲內部類,使用$作爲分割符號。這個方法會在apk中的dex包中找到對應的class文件並進行加載。
之後獲取構造方法,使用構造方法生成對應的對象。有了對象就可以使用屬性或者方法id操作對應的屬性和方法了。