*** 說明:本文不代表博主觀點,均是由以下資料整理的讀書筆記。 ***
【參考資料】
1、向您的Android Studio項目添加C/C++代碼
2、Google開發(fā)者文檔 -- 添加C++代碼到現(xiàn)有Android Studio項目中
3、JNI Tips 英文原版
4、JNI Tips 中文
5、極客學院 JNI/NDK 開發(fā)指南
6、極客學院 深入理解 JNI
7、使用CMake構建JNI環(huán)境
8、使用C和C++的區(qū)別
9、Google官方 NDK 文檔
10、極客學院 NDK開發(fā)課程
11、ndk-build 構建 JNI 環(huán)境
12、開發(fā)自己的NDK程序
13、JNI/NDK開發(fā)教程
14、JNI層修改參數值
15、JNI引用和垃圾回收
16、《Android高級進階》-- 顧浩鑫
17、《Android C++ 高級編程 -- 使用 NDK》 -- Onur Cinar
六、JNI 訪問數組
JNI 中的數組分為基本類型數組和對象數組,它們的處理方式是不一樣的,基本類型數組中的所有元素都是 JNI 的基本數據類型,可以直接訪問。而對象數組中的所有元素是一個類的實例或其它數組的引用,和字符串操作一樣,不能直接訪問 Java 傳遞給 JNI 層的數組,必須選擇合適的 JNI 函數來訪問和設置 Java 層的數組對象。
6.1 訪問基本類型數組
Java 代碼:
// 在本地代碼中求數組中所有元素的和
private native int sumArray(int[] arr);
Native 代碼:
extern "C"
JNIEXPORT jint JNICALL
Java_com_scu_miomin_learncmake_NativeLib_sumArray(
JNIEnv *env,
jclass cls,
jintArray j_array) {
jint i, sum = 0;
jint *c_array;
jint arr_len;
//1. 獲取數組長度
arr_len = env->GetArrayLength(j_array);
//2. 根據數組長度和數組元素的數據類型申請存放java數組元素的緩沖區(qū)(堆內存)
c_array = (jint *) malloc(sizeof(jint) * arr_len);
//3. 初始化緩沖區(qū)
memset(c_array, 0, sizeof(jint) * arr_len);
//4. 拷貝Java數組中的所有元素到緩沖區(qū)中
env->GetIntArrayRegion(j_array, 0, arr_len, c_array);
//5. 累加數組元素的和
for (i = 0; i < arr_len; i++) {
sum += c_array[i];
}
//6. 釋放存儲數組元素的緩沖區(qū)
free(c_array);
return sum;
}
在前面的例子當中,通過調用 GetIntArrayRegion,將 int 數組中的所有元素拷貝到 C 臨時緩沖區(qū)中,然后在本地代碼中訪問緩沖區(qū)中的元素來實現(xiàn)求和的計算。另外 JNI 還提供一系列直接獲取數組元素指針的函數 Get/ReleaseArrayElements,比如:GetIntArrayElements、ReleaseArrayElements、GetFloatArrayElements、ReleaseFloatArrayElements 等。下面我們用這種方式重新實現(xiàn)計算數組元素的和:
extern "C"
JNIEXPORT jint JNICALL
Java_com_scu_miomin_learncmake_NativeLib_sumArray2(
JNIEnv *env,
jclass cls,
jintArray j_array) {
jint i, sum = 0;
jint *c_array;
jint arr_len;
// 可能數組中的元素在內存中是不連續(xù)的,JVM可能會復制所有原始數據到緩沖區(qū),然后返回這個緩沖區(qū)的指針
c_array = env->GetIntArrayElements(j_array, NULL);
// 判斷:JVM復制原始數據到緩沖區(qū)失敗
if (c_array == NULL) {
return 0;
}
arr_len = env->GetArrayLength(j_array);
for (i = 0; i < arr_len; i++) {
sum += c_array[i];
}
// 釋放有可能存在的緩沖區(qū)
env->ReleaseIntArrayElements(j_array, c_array, 0);
return sum;
}
GetIntArrayElements 第三個參數表示返回的數組指針是原始數組,還是拷貝原始數據到臨時緩沖區(qū)的指針,如果是 JNI_TRUE:表示臨時緩沖區(qū)數組指針,JNI_FALSE:表示臨時原始數組指針。在獲取到的指針必須做校驗,因為當原始數據在內存當中不是連續(xù)存放的情況下,JVM 會復制所有原始數據到一個臨時緩沖區(qū),并返回這個臨時緩沖區(qū)的指針。有可能在申請開辟臨時緩沖區(qū)內存空間時,會內存不足導致申請失敗,這時會返回 NULL。
為了讓接口更有效率而不受VM實現(xiàn)的制約,GetArrayElements系列調用允許運行時返回一個指向實際元素的指針,或者是分配些內存然后拷貝一份。不論哪種方式,返回的原始指針在相應的Release調用之前都保證有效(這意味著,如果數據沒被拷貝,實際的數組對象將會受到牽制,不能重新成為整理堆空間的一部分)。你必須釋放(Release)每個你通過Get得到的數組。同時,如果Get調用失敗,你必須確保你的代碼在之后不會去嘗試調用Release來釋放一個空指針(NULL pointer)。
你可以用一個非空指針作為isCopy參數的值來決定數據是否會被拷貝。這相當有用。
Release類的函數接收一個mode參數,這個參數的值可選的有下面三種。而運行時具體執(zhí)行的操作取決于它返回的指針是指向真實數據還是拷貝出來的那份:
- 0
- 真實的:實際數組對象不受到牽制
- 拷貝的:數據將會復制回去,備份空間將會被釋放。
- JNI_COMMIT
- 真實的:不做任何操作
- 拷貝的:數據將會復制回去,備份空間將不會被釋放。
- JNI_ABORT
- 真實的:實際數組對象不受到牽制.之前的寫入不會被取消。
- 拷貝的:備份空間將會被釋放;里面所有的變更都會丟失。
在 Java 中創(chuàng)建的對象全都由 GC(垃圾回收器)自動回收,不需要像 C/C++ 一樣需要程序員自己管理內存。GC 會實時掃描所有創(chuàng)建的對象是否還有引用,如果沒有引用則會立即清理掉。當我們創(chuàng)建一個像 int 數組對象,本地代碼想去訪問時,發(fā)現(xiàn)這個對象正被 GC 線程占用了,這時本地代碼會一直處于阻塞狀態(tài),直到等待 GC 釋放這個對象的鎖之后才能繼續(xù)訪問。為了避免這種現(xiàn)象的發(fā)生,JNI 提供了 Get/ReleasePrimitiveArrayCritical 這對函數,本地代碼在訪問數組對象時會暫停 GC 線程。不過使用這對函數也有個限制,在 Get/ReleasePrimitiveArrayCritical 這兩個函數期間不能調用任何會讓線程阻塞或等待 JVM 中其它線程的本地函數或JNI函數,和處理字符串的 Get/ReleaseStringCritical 函數限制一樣。這對函數和 GetIntArrayElements 函數一樣,返回的是數組元素的指針。
6.2 建議
1、對于小量的、固定大小的數組,應該選擇 Get/SetArrayRegion 函數來操作數組元素是效率最高的。因為這對函數要求提前分配一個 C 臨時緩沖區(qū)來存儲數組元素,可以直接在 Stack(棧)上或用 malloc 在堆上來動態(tài)申請,當然在棧上申請是最快的。有童鞋可能會認為,訪問數組元素還需要將原始數據全部拷貝一份到臨時緩沖區(qū)才能訪問而覺得效率低?我想告訴你的是,像這種復制少量數組元素的代價是很小的,幾乎可以忽略。這對函數的另外一個優(yōu)點就是,允許你傳入一個開始索引和長度來實現(xiàn)對子數組元素的訪問和操作(SetArrayRegion函數可以修改數組),不過傳入的索引和長度不要越界,函數會進行檢查,如果越界了會拋出 ArrayIndexOutOfBoundsException 異常。
2、如果不想預先分配 C 緩沖區(qū),并且原始數組長度也不確定,而本地代碼又不想在獲取數組元素指針時被阻塞的話,使用 Get/ReleasePrimitiveArrayCritical 函數對,就像 Get/ReleaseStringCritical 函數對一樣,使用這對函數要非常小心,以免死鎖。
3、Get/ReleaseArrayElements 系列函數永遠是安全的,JVM 會選擇性的返回一個指針,這個指針可能指向原始數據,也可能指向原始數據的復制。
4、當你想做的只是拷出或者拷進數據時,可以選擇調用像GetArrayElements和GetStringChars這類非常有用的函數。想想下面:
jbyte* data = env->GetByteArrayElements(array, NULL);
if (data != NULL) {
memcpy(buffer, data, len);
env->ReleaseByteArrayElements(array, data, JNI_ABORT);
}
這里獲取到了數組,從當中拷貝出開頭的len個字節(jié)元素,然后釋放這個數組。根據代碼的實現(xiàn),Get函數將會牽制或者拷貝數組的內容。上面的代碼拷貝了數據(為了可能的第二次),然后調用Release;這當中JNI_ABORT確保不存在第三份拷貝了。
另一種更簡單的實現(xiàn)方式:
env->GetByteArrayRegion(array, 0, len, buffer);
這種方式有幾個優(yōu)點:
- 只需要調用一個JNI函數而是不是兩個,減少了開銷。
- 不需要指針或者額外的拷貝數據。
- 減少了開發(fā)人員犯錯的風險-在某些失敗之后忘記調用Release不存在風險。
6.3 訪問對象數組
JNI 提供了兩個函數來訪問對象數組,GetObjectArrayElement 返回數組中指定位置的元素,SetObjectArrayElement 修改數組中指定位置的元素。與基本類型不同的是,我們不能一次得到數據中的所有對象元素或者一次復制多個對象元素到緩沖區(qū)。因為字符串和數組都是引用類型,只能通過 Get/SetObjectArrayElement 這樣的 JNI 函數來訪問字符串數組或者數組中的數組元素。
七、C/C++ 訪問 Java 實例方法和靜態(tài)方法
** java 代碼:**
public class ClassMethod {
private static void callStaticMethod(String str, int i) {
Log.i("Miomin", "ClassMethod::callStaticMethod called!-->str=" + str + ", " + " i=" + i);
}
private void callInstanceMethod(String str) {
Log.i("Miomin", "ClassMethod::callStaticMethod called!-->str=" + str);
}
}
** JNI 代碼:**
extern "C"
JNIEXPORT void JNICALL
Java_com_scu_miomin_learncmake_NativeLib_callJavaStaticMethod(
JNIEnv *env,
jclass cls) {
jclass clazz = NULL;
jstring str_arg = NULL;
jmethodID mid_static_method;
// 1、從classpath路徑下搜索ClassMethod這個類,并返回該類的Class對象
clazz = env->FindClass("com/scu/miomin/learncmake/ClassMethod");
if (clazz == NULL) {
LOGV("Class not found : ClassMethod.class");
return;
}
// 2、從clazz類中查找callStaticMethod方法
mid_static_method = env->GetStaticMethodID(clazz, "callStaticMethod",
"(Ljava/lang/String;I)V");
if (mid_static_method == NULL) {
LOGV("Method callStaticMethod not found.");
return;
}
// 3、調用clazz類的callStaticMethod靜態(tài)方法
str_arg = env->NewStringUTF("我是靜態(tài)方法");
env->CallStaticVoidMethod(clazz, mid_static_method, str_arg, 100);
// 刪除局部引用
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(str_arg);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_scu_miomin_learncmake_NativeLib_callJavaInstaceMethod(
JNIEnv *env,
jclass cls) {
jclass clazz = NULL;
jobject jobj = NULL;
jmethodID mid_construct = NULL;
jmethodID mid_instance = NULL;
jstring str_arg = NULL;
// 1、從classpath路徑下搜索ClassMethod這個類,并返回該類的Class對象
clazz = env->FindClass("com/scu/miomin/learncmake/ClassMethod");
if (clazz == NULL) {
LOGV("Class not found : ClassMethod.class");
return;
}
// 2、獲取類的默認構造方法ID
mid_construct = env->GetMethodID(clazz, "<init>", "()V");
if (mid_construct == NULL) {
LOGV("Default constructor of ClassMethod.class not found.");
return;
}
// 3、查找實例方法的ID
mid_instance = env->GetMethodID(clazz, "callInstanceMethod", "(Ljava/lang/String;)V");
if (mid_instance == NULL) {
LOGV("Method callInstanceMethod not found.");
return;
}
// 4、創(chuàng)建該類的實例
jobj = env->NewObject(clazz, mid_construct);
if (jobj == NULL) {
LOGV("Method callInstanceMethod not found.");
return;
}
// 5、調用對象的實例方法
str_arg = env->NewStringUTF("我是實例方法");
env->CallVoidMethod(jobj, mid_instance, str_arg, 200);
// 刪除局部引用
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(jobj);
env->DeleteLocalRef(str_arg);
}
提示:其實GetMethodID的第三個參數 "(Ljava/lang/String;I)V" 指的是函數簽名,簽名規(guī)則詳見此文:http://www.cnblogs.com/CCBB/p/3978847.html
注意:雖然函數結束后,JVM 會自動釋放所有局部引用變量所占的內存空間。但還是手動釋放一下比較安全,因為在 JVM 中維護著一個引用表,用于存儲局部和全局引用變量,經測試在 Android NDK 環(huán)境下,這個表的最大存儲空間是512 個引用,如果超過這個數就會造成引用表溢出,JVM 崩潰。(局部引用和全局引用在后面的文章中會詳細介紹)
八、C/C++ 訪問 Java 實例變量和靜態(tài)變量
** Java代碼:**
public class ClassField {
private static int num;
private String str;
public int getNum() {
return num;
}
public void setNum(int num) {
ClassField.num = num;
}
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
}
** Native代碼:**
extern "C"
JNIEXPORT void JNICALL
Java_com_scu_miomin_learncmake_NativeLib_accessInstanceField(
JNIEnv *env,
jclass cls,
jobject obj) {
jclass clazz;
jfieldID fid;
jstring j_str;
jstring j_newStr;
const char *c_str = NULL;
// 1.獲取ClassField類的Class引用
clazz = env->GetObjectClass(obj);
if (clazz == NULL) {
return;
}
// 2. 獲取ClassField類實例變量str的屬性ID
fid = env->GetFieldID(clazz, "str", "Ljava/lang/String;");
// 3. 獲取實例變量str的值
j_str = (jstring) env->GetObjectField(obj, fid);
// 4. 將unicode編碼的java字符串轉換成C風格字符串
c_str = env->GetStringUTFChars(j_str, NULL);
if (c_str == NULL) {
return;
}
env->ReleaseStringUTFChars(j_str, c_str);
// 5. 修改實例變量str的值
j_newStr = env->NewStringUTF("This is C String");
if (j_newStr == NULL) {
return;
}
env->SetObjectField(obj, fid, j_newStr);
// 6.刪除局部引用
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(j_str);
env->DeleteLocalRef(j_newStr);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_scu_miomin_learncmake_NativeLib_accessStaticField(
JNIEnv *env,
jclass cls) {
jclass clazz;
jfieldID fid;
jint num;
//1.獲取ClassField類的Class引用
clazz = env->FindClass("com/scu/miomin/learncmake/ClassField");
if (clazz == NULL) { // 錯誤處理
return;
}
//2.獲取ClassField類靜態(tài)變量num的屬性ID
fid = env->GetStaticFieldID(clazz, "num", "I");
if (fid == NULL) {
return;
}
// 3.獲取靜態(tài)變量num的值
num = env->GetStaticIntField(clazz, fid);
// 4.修改靜態(tài)變量num的值
env->SetStaticIntField(clazz, fid, 80);
// 刪除屬部引用
env->DeleteLocalRef(clazz);
}
因為實例變量str是 String 類型,屬于引用類型。在 JNI 中獲取引用類型字段的值,調用 GetObjectField 函數獲取。同樣的,獲取其它類型字段值的函數還有 GetIntField,GetFloatField,GetDoubleField,GetBooleanField 等。
由于 JNI 函數是直接操作J VM 中的數據結構,不受 Java 訪問修飾符的限制。即,在本地代碼中可以調用 JNI 函數可以訪問 Java 對象中的非 public 屬性和方法。