NDK學習筆記:jni資料型別轉換
背景
隨著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操作對應的屬性和方法了。