JNI&NDK開發最佳實踐(六):JNI實現本地方法時的數據類型轉換

前言

通過《JNI數據類型及與Java數據類型的映射關系》一文,我們知道了在C中實現java聲明的本地方法時,方法入參有其一一對應關系。但在實現方法的方法體中,這些數據類型是否可以拿來直接使用呢?
舉個例子,在java文件中聲明本地方法如下

public static native void test(short s, int i, long l, float f, double d, char c,
boolean z, byte b, String str, Object obj, MyClass p, int[] arr);

其在c中的對應實現如下

JNIEXPORT void JNICALL Java_com_study_jnilearn_HelloWorld_test
(JNIEnv *env, jclass cls, jshort s, jint i, jlong l, jfloat f,
jdouble d, jchar c, jboolean z, jbyte b, jstring j_str, jobject jobj1, jobject job2, jintArray j_int_arr)
{
...
}

-基本數據類型如jint,jdouble是否可以直接操作?
-可以。
-引用數據類型如jstring,jxxArray是否可以直接操作?
-不可以。JNI接口提供了許多函數來進行這樣的轉換。

1. C方法中操作jstring

test函數接收一個jstring類型的參數str,但jstring類型是指向JVM內部的一個字符串,和C風格的字符串類型char* 不同,所以在JNI中不能通把jstring當作普通C字符串一樣來使用,必須使用合適的JNI函數來訪問JVM內部的字符串數據結構。
因為Java默認使用Unicode編碼,而C/C++默認使用UTF編碼,所以在本地代碼中操作字符串的時候,必須使用合適的JNI函數把jstring轉換成C風格的字符串。JNI支持字符串在Unicode和UTF-8兩種編碼之間轉換。
GetStringUTFChars可以把一個jstring指針(指向JVM內部的Unicode字符序列)轉換成一個UTF-8格式的C字符串。在上例中test函數中我們通過GetStringUTFChars正確取得了JVM內部的字符串內容。

1.1 操作步驟

  1. GetStringUTFChars(env, j_str, &isCopy) 訪問字符串。
    isCopy:取值JNI_TRUE和JNI_FALSE,如果值為JNI_TRUE,表示返回JVM內部源字符串的一份拷貝,并為新產生的字符串分配內存空間。如果值為JNI_FALSE,表示返回JVM內部源字符串的指針,意味著可以通過指針修改源字符串的內容,不推薦這么做,因為這樣做就打破了Java字符串不能修改的規定。但我們在開發當中,并不關心這個值是多少,通常情況下這個參數填NULL即可。

  2. 異常檢查。
    調用完GetStringUTFChars之后不要忘記安全檢查, if(c_str == NULL) { return NULL; }因為JVM需要為新誕生的字符串分配內存空間,當內存空間不夠分配的時候,會導致調用失敗,失敗后GetStringUTFChars會返回NULL,并拋出一個OutOfMemoryError異常。JNI的異常和Java中的異常處理流程是不一樣的,Java遇到異常如果沒有捕獲,程序會立即停止運行。而JNI遇到未決的異常不會改變程序的運行流程,也就是程序會繼續往下走,這樣后面針對這個字符串的所有操作都是非常危險的, 因此,我們需要用return語句跳過后面的代碼,并立即結束當前方法。

  3. 調用ReleaseStringUTFChars釋放字符串。
    在調用GetStringUTFChars函數從JVM內部獲取一個字符串之后,JVM內部會分配一塊新的內存,用于存儲源字符串的拷貝,以便本地代碼訪問和修改。即然有內存分配,用完之后馬上釋放是一個編程的好習慣。通過調用ReleaseStringUTFChars函數通知JVM這塊內存已經不使用了,你可以清除了。注意:這兩個函數是配對使用的,用了GetXXX就必須調用ReleaseXXX。

  4. 調用NewStringUTF函數,創建字符串返回給java。
    通過調用NewStringUTF函數,會構建一個新的java.lang.String字符串對象。這個新創建的字符串會自動轉換成Java支持的Unicode編碼。如果JVM不能為構造java.lang.String分配足夠的內存,NewStringUTF會拋出一個OutOfMemoryError異常,并返回NULL。在這個例子中我們不必檢查它的返回值,如果NewStringUTF創建java.lang.String失敗,OutOfMemoryError這個異常會被在Sample.main方法中拋出。如果NewStringUTF創建java.lang.String成功,則返回一個JNI引用,這個引用指向新創建的java.lang.String對象。

1.2 示例代碼


JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello
  (JNIEnv *env, jclass cls, jstring j_str)
{
    const char *c_str = NULL;
    char buff[128] = {0};
    jboolean isCopy;    // 返回JNI_TRUE表示原字符串的拷貝,返回JNI_FALSE表示返回原字符串的指針
    c_str = (*env)->GetStringUTFChars(env, j_str, &isCopy);
    printf("isCopy:%d\n",isCopy);
    if(c_str == NULL)
    {
        return NULL;
    }
    printf("C_str: %s \n", c_str);
    sprintf(buff, "hello %s", c_str);
    (*env)->ReleaseStringUTFChars(env, j_str, c_str);
    return (*env)->NewStringUTF(env,buff);
}

1.3 其它字符串處理函數

1> GetStringChars和ReleaseStringChars:這對函數和Get/ReleaseStringUTFChars函數功能差不多,用于獲取和釋放以Unicode格式編碼的字符串。后者是用于獲取和釋放UTF-8編碼的字符串。

2> GetStringLength:由于UTF-8編碼的字符串以'\0'結尾,而Unicode字符串不是。如果想獲取一個指向Unicode編碼的jstring字符串長度,在JNI中可通過這個函數獲取。

3> GetStringUTFLength:獲取UTF-8編碼字符串的長度,也可以通過標準C函數strlen獲取。

4> GetStringCritical和ReleaseStringCritical:提高JVM返回源字符串直接指針的可能性。

5> GetStringRegion和GetStringUTFRegion:分別表示獲取Unicode和UTF-8編碼字符串指定范圍內的內容。這對函數會把源字符串復制到一個預先分配的緩沖區內。

2. C 方法中操作數組

NI中的數組分為基本類型數組和對象數組,它們的處理方式是不一樣的,基本類型數組中的所有元素都是JNI的基本數據類型,可以直接訪問。而對象數組中的所有元素是一個類的實例或其它數組的引用,和字符串操作一樣,不能直接訪問Java傳遞給JNI層的數組,必須選擇合適的JNI函數來訪問和設置Java層的數組對象。

2.1 操作基本類型數組

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_study_jnilearn_IntArray */
 
#ifndef _Included_com_study_jnilearn_IntArray
#define _Included_com_study_jnilearn_IntArray
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_study_jnilearn_IntArray
 * Method:    sumArray
 * Signature: ([I)I
 */
JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray
  (JNIEnv *, jobject, jintArray);
 
#ifdef __cplusplus
}
#endif
#endif
 
// IntArray.c
#include "com_study_jnilearn_IntArray.h"
#include <string.h>
#include <stdlib.h>
 
/*
 * Class:     com_study_jnilearn_IntArray
 * Method:    sumArray
 * Signature: ([I)I
 */
JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray
(JNIEnv *env, jobject obj, jintArray j_array)
{
    jint i, sum = 0;
    jint *c_array;
    jint arr_len;
    //1. 獲取數組長度
    arr_len = (*env)->GetArrayLength(env,j_array);
    //2. 根據數組長度和數組元素的數據類型申請存放java數組元素的緩沖區
    c_array = (jint*)malloc(sizeof(jint) * arr_len);
    //3. 初始化緩沖區
    memset(c_array,0,sizeof(jint)*arr_len);
    printf("arr_len = %d ", arr_len);
    //4. 拷貝Java數組中的所有元素到緩沖區中
    (*env)->GetIntArrayRegion(env,j_array,0,arr_len,c_array);
    for (i = 0; i < arr_len; i++) {
        sum += c_array[i];  //5. 累加數組元素的和
    }
    free(c_array);  //6. 釋放存儲數組元素的緩沖區
    return sum;
}

2.2 操作引用數據類型數組

JNI提供了兩個函數來訪問對象數組,GetObjectArrayElement 返回數組中指定位置的元素,SetObjectArrayElement 修改數組中指定位置的元素。與基本類型不同的是,我們不能一次得到數據中的所有對象元素或者一次復制多個對象元素到緩沖區。因為字符串和數組都是引用類型,只能通過 Get/SetObjectArrayElement 這樣的JNI函數來訪問字符串數組或者數組中的數組元素。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_study_jnilearn_ObjectArray */
 
#ifndef _Included_com_study_jnilearn_ObjectArray
#define _Included_com_study_jnilearn_ObjectArray
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_study_jnilearn_ObjectArray
 * Method:    initInt2DArray
 * Signature: (I)[[I
 */
JNIEXPORT jobjectArray JNICALL Java_com_study_jnilearn_ObjectArray_initInt2DArray
  (JNIEnv *, jobject, jint);
 
#ifdef __cplusplus
}
#endif
#endif
 
 
// ObjectArray.c
 
#include "com_study_jnilearn_ObjectArray.h"
/*
 * Class:     com_study_jnilearn_ObjectArray
 * Method:    initInt2DArray
 * Signature: (I)[[I
 */
JNIEXPORT jobjectArray JNICALL Java_com_study_jnilearn_ObjectArray_initInt2DArray
  (JNIEnv *env, jobject obj, jint size)
{
    jobjectArray result;
    jclass clsIntArray;
    jint i,j;
    // 1.獲得一個int型二維數組類的引用
    clsIntArray = (*env)->FindClass(env,"[I");
    if (clsIntArray == NULL)
    {
        return NULL;
    }
    // 2.創建一個數組對象(里面每個元素用clsIntArray表示)
    result = (*env)->NewObjectArray(env,size,clsIntArray,NULL);
    if (result == NULL)
    {
        return NULL;
    }
    
    // 3.為數組元素賦值
    for (i = 0; i < size; ++i)
    {
        jint buff[256];
        jintArray intArr = (*env)->NewIntArray(env,size);
        if (intArr == NULL)
        {
            return NULL;
        }
        for (j = 0; j < size; j++)
        {
            buff[j] = i + j;
        }
        (*env)->SetIntArrayRegion(env,intArr, 0,size,buff);
        (*env)->SetObjectArrayElement(env,result, i, intArr);
        (*env)->DeleteLocalRef(env,intArr);
    }
    
    return result;
}

3. 總結

對于引用類型(jObject),將參數轉換或復制為本地類型,例如將jstring轉換為C字符串,將jintArray轉換為C的int []等;對于原始JNI類型,如jint和jdouble不需要轉換,可以直接操作。

更多JNI&NDK系列文章,參見:
JNI&NDK開發最佳實踐(一):開篇
JNI&NDK開發最佳實踐(二):CMake實現調用已有C/C++文件中的本地方法
JNI&NDK開發最佳實踐(三):CMake實現調用已有so庫中的本地方法
JNI&NDK開發最佳實踐(四):JNI數據類型及與Java數據類型的映射關系
JNI&NDK開發最佳實踐(五):本地方法的靜態注冊與動態注冊
JNI&NDK開發最佳實踐(六):JNI實現本地方法時的數據類型轉換
JNI&NDK開發最佳實踐(七):JNI之本地方法與java互調
JNI&NDK開發最佳實踐(八):JNI局部引用、全局引用和弱全局引用
JNI&NDK開發最佳實踐(九):調試篇

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380