前言
通過《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 操作步驟
GetStringUTFChars(env, j_str, &isCopy) 訪問字符串。
isCopy:取值JNI_TRUE和JNI_FALSE,如果值為JNI_TRUE,表示返回JVM內部源字符串的一份拷貝,并為新產生的字符串分配內存空間。如果值為JNI_FALSE,表示返回JVM內部源字符串的指針,意味著可以通過指針修改源字符串的內容,不推薦這么做,因為這樣做就打破了Java字符串不能修改的規定。但我們在開發當中,并不關心這個值是多少,通常情況下這個參數填NULL即可。異常檢查。
調用完GetStringUTFChars之后不要忘記安全檢查,if(c_str == NULL) { return NULL; }
因為JVM需要為新誕生的字符串分配內存空間,當內存空間不夠分配的時候,會導致調用失敗,失敗后GetStringUTFChars會返回NULL,并拋出一個OutOfMemoryError異常。JNI的異常和Java中的異常處理流程是不一樣的,Java遇到異常如果沒有捕獲,程序會立即停止運行。而JNI遇到未決的異常不會改變程序的運行流程,也就是程序會繼續往下走,這樣后面針對這個字符串的所有操作都是非常危險的, 因此,我們需要用return語句跳過后面的代碼,并立即結束當前方法。調用ReleaseStringUTFChars釋放字符串。
在調用GetStringUTFChars函數從JVM內部獲取一個字符串之后,JVM內部會分配一塊新的內存,用于存儲源字符串的拷貝,以便本地代碼訪問和修改。即然有內存分配,用完之后馬上釋放是一個編程的好習慣。通過調用ReleaseStringUTFChars函數通知JVM這塊內存已經不使用了,你可以清除了。注意:這兩個函數是配對使用的,用了GetXXX就必須調用ReleaseXXX。調用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開發最佳實踐(九):調試篇