JNI 入門
1 JNI和NDK介紹
JNI(Java Native Interface),是方便Java呼叫C、C++等Native程式碼所封裝的 一層介面 ,相當於一座橋樑。通過JNI可以操作一些Java無法完成的與系統相關的特性,尤其在影象和視訊處理中大量用到。
NDK(Native Development Kit)是Google提供的一套工具,其中一個特性是提供了交叉編譯,即C或者C++不是跨平臺的,但通過NDK配置生成的動態庫卻可以相容各個平臺。比如C在Windows平臺編譯後生成.exe檔案,那麼原始碼通過NDK編譯後可以生成在安卓手機上執行的二進位制檔案.so
1.1 AS環境配置
- 下載NDK

image
-
配置環境變數
sudo gedit/etc/profile 增加如下內容 export ANDROID_NDK_ROOT=/home/rentianxin/android-sdk-linux/ndk-bundle export PATH=$PATH:$ANDROID_NDK_ROOT
-
修改build.gradle 增加配置
externalNativeBuild { ndkBuild { path 'src/main/jni/Android.mk' } }
1.2 在AS中使用ndk-build開發JNI示例
Android Studio2.2之前對於JNI開發的支援不是很好,開發一般使用Eclipse+外掛編寫本地動態庫。後面Google官方全面增強了對JNI的支援,包括內建NDK。
1.2.1 在AS中新建一個專案
1.2.2 宣告一個native方法
package com.richy.richydemo.jni; public class JNITest { public native static String getStrFromJNI(); }
1.2.3 通過javah命令生成標頭檔案
rentianxin@rentianxin-Desk ~/A/RichyDemo> cd mobile/src/main/java/ rentianxin@rentianxin-Desk ~/A/R/m/s/m/java> javah -jni com.richy.richydemo.jni.JNITest
生成標頭檔案 com_richy_richydemo_jni_JNITest.h
實際專案最終可以不包含此標頭檔案,不熟悉C的語法的開發人員,藉助於該標頭檔案可以知道JNI的相關語法:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /*首先引入jni.h,裡面包含了很多巨集定義及呼叫本地方法的結構體*/ /* Header for class com_richy_richydemo_jni_JNITest */ #ifndef _Included_com_richy_richydemo_jni_JNITest #define _Included_com_richy_richydemo_jni_JNITest #ifdef __cplusplus extern "C" { #endif /* * Class:com_richy_richydemo_jni_JNITest * Method:getStrFromJNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_richy_richydemo_jni_JNITest_getStrFromJNI (JNIEnv *, jclass);/*jclass是jni.h中定義好的,型別是jobject,實際上是一個不確定型別的指標,這裡用來接收Java中的this*/ #ifdef __cplusplus } #endi
首先引入jni.h,裡面包含了很多巨集定義及呼叫本地方法的結構體。重點是方法名的格式。這裡的JNIEXPORT和JNICALL都是jni.h中所定義的巨集。JNIEnv *表示一個指向JNI環境的指標,可通過它來訪問JNI提供的介面方法。jclass也是jni.h中定義好的,型別是jobject,實際上是一個不確定型別的指標,這裡用來接收Java中的this。實際編寫中一般只要遵循 Java_包名_類名_方法名
就好了。
1.2.4 編寫mk檔案實現JNI方法
像上面的標頭檔案只是定義了方法,並沒有實現,就像一個介面一樣。這裡就用C寫一個簡單的無參的JNI方法。
先建立一個jni目錄,我直接在src的父目錄下建立的,也可以在其他目錄建立,因為最終只需要編譯好的動態庫。在jni目錄下建立Android.mk和demo.c檔案。

image.png
Android.mk是一個makefile配置檔案,安卓大量採用makefile進行自動化編譯。LOCAL_MODULE定義的名稱就是編譯好的so庫名稱,比如這裡是 jni-demo
,最終生成的動態庫名稱就叫libjni-demo.so。 LOCAL_SRC_FILES表示參與編譯的原始檔名稱,這裡就是demo.c
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := jni-demo LOCAL_SRC_FILES := demo.c include $(BUILD_SHARED_LIBRARY)
(可選)編寫 Application.mk :
# 指定生成哪些cpu架構的庫 APP_ABI := armeabi-v7a # 此變數包含目標 Android 平臺的名稱 APP_PLATFORM := android-22
這裡的demo.c實現了一個很簡單的方法,返回String型別。方法名是從前面生成的com_richy_richydemo_jni_JNITest.h直接拷貝。
#include<jni.h> jstring Java_com_mercury_jnidemo_JNITest_getStrFromJNI(JNIEnv *env,jobject thiz){ return (*env)->NewStringUTF(env,"I am Str from jni libs!"); }
也可以依賴之前生成的.h檔案
#include<com_richy_richydemo_jni_JNITest.h> JNIEXPORT jstring JNICALL Java_com_richy_richydemo_jni_JNITest_getStrFromJNI (JNIEnv *env, jclass thiz) { return (*env)->NewStringUTF(env, "I am Str from jni libs!"); }
這時候NDK編譯生成的動態庫會有四個CPU平臺:arm64-v8a、armeabi-v7a、x86、x86_64。如果建立Application.mk就可以指定要生成的CPU平臺,語法也很簡單:
APP_ABI := all
這樣就會生成各個CPU平臺下的動態庫。
1.2.5 使用ndk-build程式設計生成.so庫
切回到jni目錄的父目錄下,在Terminal中執行ndk-build指令,就可以在和jni目錄同級生成一個libs資料夾,裡面存放相對應的平臺的.so庫。同時生成的還有一箇中間臨時的obj資料夾,和jni資料夾可以一起刪除。
rentianxin@rentianxin-Desk ~/A/R/m/s/main> ndk-build Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-16. Android NDK: WARNING: APP_PLATFORM android-16 is higher than android:minSdkVersion 1 in ./AndroidManifest.xml. NDK binaries will *not* be compatible with devices older than android-16. See https://android.googlesource.com/platform/ndk/+/master/docs/user/common_problems.md for more information. [arm64-v8a] Compile: jni-demo <= demo.c [arm64-v8a] SharedLibrary: libjni-demo.so [arm64-v8a] Install: libjni-demo.so => libs/arm64-v8a/libjni-demo.so [armeabi-v7a] Compile thumb: jni-demo <= demo.c [armeabi-v7a] SharedLibrary: libjni-demo.so [armeabi-v7a] Install: libjni-demo.so => libs/armeabi-v7a/libjni-demo.so ...

生成檔案
需要注意,使用NDK一定要先在build.gradle下要配置ndk-build的相關路徑,這樣在編寫原生代碼時才會有相關的提示功能,並且可以關聯到相關的標頭檔案:
externalNativeBuild { ndkBuild { path 'src/main/jni/Android.mk' } }
1.2.6 載入.so庫並呼叫方法
在類初始化的時候要載入該.so庫,一般會寫在靜態程式碼塊裡。名稱就是前面的LOCAL_MODULE。
public class JNITest { static { System.loadLibrary("jni-demo"); } public native static String getStrFromJNI(); }
需要注意的是如果是有參的JNI方法,那麼直接在引數列表裡補充在jni.h預先typedef好的資料型別就可以了。
public class JniTestActivityActivity extends BaseActivity { ... @OnClick(R.id.btn_load) public void onClick() { final String strFromJNI = JNITest.getStrFromJNI(); logd(strFromJNI); mtvText.setText(strFromJNI); } }
1.3 在AS中使用使用CMake開發JNI
CMake是一個跨平臺的安裝(編譯)工具,通過編寫CMakeLists.txt,可以生成對應的makefile或project檔案,再呼叫底層的編譯。AS 2.2之後工具中增加了對CMake的支援,官方也推薦用CMake+CMakeLists.txt的方式,代替ndk-build+Android.mk+Application.mk的方式去構建JNI專案.
1.3.1 建立使用CMake構建的專案
開始前AS要先在SDK Manager中安裝SDK Tools->CMake

image
只要勾選 Include C++ Support
。其中會提示配置C++支援的功能.

image.png
1.3.2 工程的目錄結構

image
,相當於以前的配置檔案。並且在src/main目錄下多了一個cpp資料夾,裡面存放的是C++檔案,相當於以前的jni資料夾。這個是工程建立後AS生成的示例JNI方法,返回了一個字串。後面開發JNI就可以按照這個目錄結構。
相應的,build.gradle下也增加了一些配置。
android { defaultConfig { ... externalNativeBuild { cmake { cppFlags "" } } } ... externalNativeBuild { cmake { path "CMakeLists.txt" } } }
defaultConfig中的externalNativeBuild各項屬性和前面建立專案時的選項配置有關,外部的externalNativeBuild則定義了CMakeLists.txt的存放路徑。
如果只是在自己的專案中使用,CMake的方式在打包APK的時候會自動將cpp檔案編譯成so檔案拷貝進去。如果要提供給外部使用時, Make Project
,之後在 libs
目錄下就可以看到生成的對應配置的相關CPU平臺的.so檔案。
1.3.3 CMakeLists.txt
CMakeLists.txt可以自定義命令、查詢檔案、標頭檔案包含、設定變數,具體可見 ofollow,noindex">官方文件 。專案預設生成的CMakeLists.txt核心內容如下:
# 編譯本地庫時我們需要的最小的cmake版本 cmake_minimum_required(VERSION 3.4.1) # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. # Gradle automatically packages shared libraries with your APK. # 相當於Android.mk add_library( # Sets the name of the library. native-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). src/main/cpp/native-lib.cpp ) # Searches for a specified prebuilt library and stores the path as a # variable. Because CMake includes system libraries in the search path by # default, you only need to specify the name of the public NDK library # you want to add. CMake verifies that the library exists before # completing its build. # 新增一些我們在編譯我們的本地庫的時候需要依賴的一些庫,這裡是用來打log的庫 find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log ) # Specifies libraries CMake should link to your target library. You # can link multiple libraries, such as libraries you define in this # build script, prebuilt third-party libraries, or system libraries. # 關聯自己生成的庫和一些第三方庫或者系統庫,先新增再關聯 target_link_libraries( # Specifies the target library. native-lib # Links the target library to the log library # included in the NDK. ${log-lib}
直接編譯專案只會在中間檔案中產生so檔案,如果想要明確看到生成的so,可以在CMakeLists.txt中指定so庫的輸出路徑,這樣編譯後就會在libs目錄下生成相應so檔案,但一定要在add_library之前設定,否則不會生效:
#指定路徑 #生成的so庫在和CMakeLists.txt同級目錄下的libs資料夾下 set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI})
如果想要配置so庫的目標CPU平臺,可以在build.gradle中設定
android { ... defaultConfig { ... ndk{ abiFilters "x86","armeabi","armeabi-v7a" } } ... }
需要注意的是,如果是多次使用add_library,則會生成多個so庫。如果想將多個本地檔案編譯到一個so庫中,只要最後一個引數新增多個C/C++檔案的相對路徑就可以
1.3.4 示例-用C語言實現字串加密
Java中實現字串加密的一種比較簡單的方法是異或,將字串轉換為字元陣列,遍歷對其中的每個字元用金鑰(可以是字元)進行一次異或運算,生成新的字串。如果用JNI和C實現,大致步驟如下(jstring是要加密的字串):
-
獲取jstring的長度
-
動態開闢一個跟data長度一樣的char*
-
將jstring型別轉換為char陣列(用char*接收)
-
遍歷char陣列,進行異或運算
-
將char*轉換為jstring型別返回
-
釋放動態開闢的堆記憶體空間
完整程式碼如下:
//CMakeDemo/app/src/main/cpp/native-lib.c #include<jni.h> #include <stdlib.h> jboolean checkUtfBytes(const char *bytes, const char **errorKind); jstring Java_com_richy_cmakedemo_MainActivity_encryptStr (JNIEnv *env, jobject object, jstring data) { if (data == NULL) {//字串為空 return (*env)->NewStringUTF(env, ""); } jsize len = (*env)->GetStringLength(env, data);//得到字串長度 char *buffer = (char *) malloc(len * sizeof(char));//分配記憶體控制元件 char * (*env)->GetStringUTFRegion(env, data, 0, len, buffer);//將data放入buffer int i = 0; for (; i < len; i++) { buffer[i] = (char) (buffer[i] ^ 2);//和2異或 } const char *errorKind = NULL; checkUtfBytes(buffer, &errorKind);//排除非utf-8字元,errorKind會改變 free(buffer);//釋放記憶體 if (errorKind == NULL) { return (*env)->NewStringUTF(env, buffer); } else { return (*env)->NewStringUTF(env, ""); } } //把char*和errorKind傳入,如果errorKind不為NULL說明含有非utf-8字元,做相應處理 //char **是二級char指標,表示字串陣列(第一級為字串,第二級為陣列 jboolean checkUtfBytes(const char *bytes, const char **errorKind) { while (*bytes != '\0') { jboolean utf8 = *(bytes++); // Switch on the high four bits. switch (utf8 >> 4) { case 0x00: case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: // Bit pattern 0xxx. No need for any extra bytes. break; case 0x08: case 0x09: case 0x0a: case 0x0b: case 0x0f: /* * Bit pattern 10xx or 1111, which are illegal start bytes. * Note: 1111 is valid for normal UTF-8, but not the * modified UTF-8 used here. */ *errorKind = "start"; return utf8; case 0x0e: // Bit pattern 1110, so there are two additional bytes. utf8 = *(bytes++); if ((utf8 & 0xc0) != 0x80) { *errorKind = "continuation"; return utf8; } // Fall through to take care of the final byte. case 0x0c: case 0x0d: // Bit pattern 110x, so there is one additional byte. utf8 = *(bytes++); if ((utf8 & 0xc0) != 0x80) { *errorKind = "continuation"; return utf8; } break; } } return 0; }
1.4 JNIEnv 是什麼
JNIEnv 是一個指向全部 JNI方法的指標 ,該指標只在建立它的執行緒有效,不能跨執行緒傳遞,因此,不同執行緒的JNIEnv是彼此獨立的,JNIEnv的主要作用有兩點:
1.呼叫Java的方法。
2.操作Java(獲取Java中的變數和物件等等)。
先來看JNIEnv的定義,如下所示。
libnativehelper/include/nativehelper/jni.h
#if defined(__cplusplus) typedef _JNIEnv JNIEnv;//C++中JNIEnv的型別 typedef _JavaVM JavaVM; #else typedef const struct JNINativeInterface* JNIEnv;//C中JNIEnv的型別 typedef const struct JNIInvokeInterface* JavaVM; #endif
這裡使用預定義巨集 __cplusplus
來區分C和C++兩種程式碼,如果定義了 __cplusplus
,則是C++程式碼中的定義,否則就是C程式碼中的定義。
在這裡我們也看到了JavaVM,它是虛擬機器在JNI層的代表,在一個虛擬機器程序中只有一個JavaVM,因此,該程序的所有執行緒都可以使用這個JavaVM。
通過JavaVM的AttachCurrentThread函式可以獲取這個執行緒的JNIEnv,這樣就可以在不同的執行緒中呼叫Java方法了。還要記得在使用AttachCurrentThread函式的執行緒退出前,務必要呼叫DetachCurrentThread函式來釋放資源。
2 JNI 傳遞引數和返回值
在native層實現 getStrFromJNI 靜態方法;
JNIEXPORT jstring JNICALL Java_com_richy_richydemo_jni_JNITest_getStrFromJNI (JNIEnv *, jclass);
由此看出,每個native函式,都至少有兩個引數(JNIEnv*,jclass或者jobject)
- 當native方法為靜態方法時:
jclass 代表native方法所屬類的class物件(JniTest.class) - 當native方法為非靜態方法時:
jobject 代表native方法所屬的物件
2.1 Java基本資料型別傳遞
在Java世界中可以使用boolean,byte,char,short,int,long,float,double,但是native世界可沒有這些型別,對應的jni中提供了相應的替代,如jboolean,jbyte,jchar,jshort,jint,jlong,jfloat,jdouble 等,這幾種型別幾乎都可以當成對應的C++型別來用。
Java基本資料型別與JNI資料型別的對映關係如下:
2.1.1 基本資料型別的轉換
Java | Native | Signature |
---|---|---|
byte | jbyte | B |
char | jchar | C |
double | jdouble | D |
float | jfloat | F |
int | jint | I |
short | jshort | S |
long | jlong | J |
boolean | jboolean | Z |
void | void | V |
從上表可以可看出,基本資料型別轉換,除了void,其他的資料型別只需要在前面加上“j”就可以了。第三列的Signature 代表簽名格式,後文會介紹它。接著來看引用資料型別的轉換。
2.1.2 引用資料型別的轉換
Java | Native | Signature |
---|---|---|
所有物件 | jobject | L+classname +; |
Class | jclass | Ljava/lang/Class; |
String | jstring | Ljava/lang/String; |
Throwable | jthrowable | Ljava/lang/Throwable; |
Object[] | jobjectArray | [L+classname +; |
byte[] | jbyteArray | [B |
char[] | jcharArray | [C |
double[] | jdoubleArray | [D |
float[] | jfloatArray | [F |
int[] | jintArray | [I |
short[] | jshortArray | [S |
long[] | jlongArray | [J |
boolean[] | jbooleanArray | [Z |
從上表可一看出,陣列的JNI層資料型別需要以“Array”結尾,簽名格式的開頭都會有“[”。除了陣列以外,其他的引用資料型別的簽名格式都會以“;”結尾。
另外,引用資料型別還具有繼承關係,如下所示:

image
2.2 引數傳遞示例
2.2.1 String引數的傳遞
JNIEXPORT jstring JNICALL Java_com_richy_cmakedemo_JniTest_getLine(JNIEnv *env, jobject instance, jstring prompt_) { //將java字串轉化為c語言可以識別的字串 const char *prompt = env->GetStringUTFChars(prompt_, 0); if (prompt == NULL) { return NULL; } std::cout << prompt << std::endl; //釋放資源 env->ReleaseStringUTFChars(prompt_, prompt); char *temStr = "return string"; //編碼成java字串 jstring rtStr = env->NewStringUTF(temStr); return rtStr; }
2.2.2 陣列引數的傳遞
int compare(int *a, int *b) { return (*a) - (*b); } extern "C" JNIEXPORT void JNICALL Java_com_richy_cmakedemo_JniTest_giveArray(JNIEnv *env, jobject instance, jintArray array_) { //jintArray -> jint指標 -> c int 陣列 jint *array = env->GetIntArrayElements(array_, NULL); //printf("%#x,%#x\n", &array, &array_); //陣列的長度 int len = env->GetArrayLength(array_); //排序 qsort(array, len, sizeof(jint), compare); //同步 //mode //0, Java陣列進行更新,並且釋放C/C++陣列 //JNI_ABORT, Java陣列不進行更新,但是釋放C/C++陣列 //JNI_COMMIT,Java陣列進行更新,不釋放C/C++陣列(函式執行完,陣列還是會釋放) env->ReleaseIntArrayElements(array_, array, JNI_COMMIT); }
這個程式碼中的 GetIntArrayElements 和 ReleaseIntArrayElements 函式就是JNI提供用於處理int陣列的函式。
如果用arr[i]的方式去訪問jintArray型別,不用問肯定會出錯。
JNI還提供了另一對函式 GetIntArrayRegion 和 ReleaseIntArrayRegion 訪問int陣列,不在這裡做介紹,至於其他的型別陣列,方法類似。
2.2.3 返回陣列
extern "C" JNIEXPORT jintArray JNICALL Java_com_richy_cmakedemo_JniTest_getArray(JNIEnv *env, jobject instance, jint len) { //建立一個指定大小的陣列 jintArray jint_arr = env->NewIntArray(len); jint *elems = env->GetIntArrayElements(jint_arr, NULL); int i = 0; for (; i < len; i++) { elems[i] = i; } //同步 env->ReleaseIntArrayElements(jint_arr, elems, 0); return jint_arr; }
3 JNI 呼叫Java屬性和方法
3.1 訪問類、物件和方法
初始化了Java虛擬機器後,native就可以呼叫Java的方法,要呼叫一個Java物件的方法必須經過幾個步驟:
3.1.1 獲取指定物件的類定義(jclass)
有兩種方式來獲取物件的類定義:
第一種是在已知類名的情況下使用FindClass來查詢對應的類。但是要注意類名並不同於平時寫的Java程式碼,例如要得到類jni.test.Demo的定義必須呼叫如下程式碼:
jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把點號換成斜槓
第二種是通過物件直接得到其所對應的類定義:
jclass cls = (*env)-> GetObjectClass(env, obj); //其中obj是要引用的物件,型別是jobject
//建立Date物件 jclass cls = env->FindClass("java/util/Date"); jmethodID constructor_mid = env->GetMethodID(cls, "<init>", "()V"); //新建物件/變數 jobject obj = env->NewObject(cls, constructor_mid);
3.1.2 讀取要呼叫方法的定義
我們先來看看JNI中獲取方法定義的函式:
jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass class, const char *name, const char *sig);
這兩個函式的區別明顯都能猜到, GetStaticMethodID
是用來獲取靜態方法的定義,而 GetMethodID
則是獲取非靜態的方法定義。
這兩個函式都需要提供四個引數:
- 第一個引數env 就是初始化虛擬機器得到的jni環境;
- 第二個引數class 是物件的類定義,也就是第一步得到的obj;
- 第三個引數是方法名稱;
- 第四個引數是方法簽名。
我們知道Java是有過載方法的,可以定義方法名相同,但引數不同的方法,正因為如此,在JNI中僅僅通過方法名是無法找到 Java中的具體方法的,JNI為了解決這一問題就將引數型別和返回值型別組合在一起作為方法簽名。通過方法簽名和方法名就可以找到對應的Java方法。
JNI的方法簽名的格式為:
(引數簽名格式...)返回值簽名格式
如果我們每次編寫JNI時都要寫方法簽名,也會是一件比較頭疼的事,Java提供了javap命令來自動生成方法簽名。
rentianxin@rentianxin-Desk ~/A/R/C/a/s/m/j/c/r/cmakedemo> javac JniTest.java rentianxin@rentianxin-Desk ~/A/R/C/a/s/m/j/c/r/cmakedemo> ls JniTest.classJniTest.javaMainActivity.java rentianxin@rentianxin-Desk ~/A/R/C/a/s/m/j/c/r/cmakedemo> javap -s -p JniTest.class Compiled from "JniTest.java" public class com.richy.cmakedemo.JniTest { public com.richy.cmakedemo.JniTest(); descriptor: ()V public static native java.lang.String getStringFromC(); descriptor: ()Ljava/lang/String; public native java.lang.String getString2FromC(int); descriptor: (I)Ljava/lang/String; private native java.lang.String getLine(java.lang.String); descriptor: (Ljava/lang/String;)Ljava/lang/String; public native void giveArray(int[]); descriptor: ([I)V public native int[] getArray(int); descriptor: (I)[I public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V static {}; descriptor: ()V }
3.1.3 呼叫方法
獲取到方法的定義jmethodID後,就可以呼叫方法了。為了呼叫物件的某個方法,可以使用函式
Call<TYPE>Method 或者 CallStatic<TYPE>Method(訪問類的靜態方法)
<TYPE>根據不同的返回型別而定。這些方法都是使用可變引數的定義,如果訪問某個方法需要引數時,只需要把所有引數按照順序填寫到方法中就可以。在講到建構函式的訪問時,將演示如何訪問帶引數的建構函式。
3.3 訪問類屬性
3.3.1 獲取指定物件的類(jclass)
這一步,與訪問類方法完全一樣。
jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把點號換成斜槓
3.3.2 讀取類屬性的定義(jfieldID)
在JNI中是這樣定義獲取 類屬性 的方法的:
jfieldID (JNICALL *GetFieldID) (JNIEnv *env, jclass clazz, const char *name, const char *sig); jfieldID (JNICALL *GetStaticFieldID) (JNIEnv *env, jclass clazz, const char *name, const char *sig);
這兩個函式中第一個引數為JNI環境;clazz為類的定義;name為屬性名稱;第四個引數同樣是為了表達屬性的型別。前面我們使用javap工具獲取類的詳細定義的時候有這樣兩行:
public java.lang.String key;
/* Ljava/lang/String; */
其中第二行註釋的內容就是第四個引數要填的資訊,這跟訪問類方法時是相同的。
3.3.3 讀取物件的屬性和設定屬性值
獲取到屬性的定義fieldID後,就可以訪問屬性值了。有幾個方法用來讀取和設定類的屬性,它們是:
Get<TYPE>Field、 Set<TYPE>Field、GetStatic<TYPE>Field、 SetStatic<TYPE>Field
。
JNIEXPORT jstring JNICALL Java_com_richy_cmakedemo_JniTest_accessField(JNIEnv *env, jobject instance) { //jobj是t物件,JniTest.class jclass cls = env->GetObjectClass(instance); //jfieldID //屬性名稱,屬性簽名 jfieldID fid = env->GetFieldID(cls, "key", "Ljava/lang/String;"); //richy >> super richy //獲取key屬性的值 //Get<Type>Field jstring jstr = static_cast<jstring>(env->GetObjectField(instance, fid)); printf("jstr:%#x\n",&jstr); //jstring -> c字串 //isCopy 是否複製(true代表賦值,false不復制) char *c_str = const_cast<char *>(env->GetStringUTFChars(jstr, JNI_FALSE)); //拼接得到新的字串 char text[20] = "super "; strcat(text,c_str); //c字串 ->jstring jstring new_jstr = env->NewStringUTF(text); //修改key //Set<Type>Field env->SetObjectField(instance, fid, new_jstr); printf("new_jstr:%#x\n", &new_jstr); return new_jstr; }
4 記憶體管理
對於Java程式員來說,記憶體管理是完全透明的,Java虛擬機器會處理。然而從Java虛擬機器建立的物件傳到C/C++程式碼時會產生引用,根據Java的垃圾回收機制,只要有引用存在就不會觸發該引用所指向Java物件的垃圾回收。
這些引用在 JNI 中分為3種: 全域性引用 (Global Reference)、 區域性引用 (Local Reference)、 弱全域性引用 (Week Global Reference- since JDK1.2)。
4.1 三種引用的區別
4.1.1 全域性引用
全域性引用可以跨方法、跨執行緒使用,直到被開發者顯式釋放。類似區域性引用,一個全域性引用在被釋放前保證引用物件不被GC回收。和區域性引用不同的是,沒有那麼多函式能夠建立全域性引用。能建立全域性引用的函式只有 NewGlobalRef。以下例子說明了如何使用一個全域性引用。
extern "C" JNIEXPORT void JNICALL Java_com_richy_cmakedemo_JniTest_createGlobalRef(JNIEnv *env, jobject instance) { jstring obj = env->NewStringUTF("jni development is powerful!"); global_str = static_cast<jstring>(env->NewGlobalRef(obj)); } extern "C" JNIEXPORT jstring JNICALL Java_com_richy_cmakedemo_JniTest_getGlobalRef(JNIEnv *env, jobject instance) { return global_str; } extern "C" JNIEXPORT void JNICALL Java_com_richy_cmakedemo_JniTest_deleteGlobalRef(JNIEnv *env, jobject instance) { env->DeleteGlobalRef(global_str); } /*記憶體引用研究---------------end--------------*/
4.1.2 區域性引用
一個區域性引用僅在建立它的native函式及該函式呼叫的函式中有效。在一個native函式執行期間建立的所有區域性引用將在 該函式返回時被釋放 。
示例建立了大量的區域性引用,佔用了太多的記憶體,而且這些區域性引用跟後面的操作沒有關聯性,就可以提前釋放。
JNIEXPORT void JNICALL Java_com_richy_cmakedemo_JniTest_localRef(JNIEnv *env, jobject instance) { int i = 0; for (; i < 5; i++){ //建立Date物件 jclass cls = env->FindClass("java/util/Date"); jmethodID constructor_mid = env->GetMethodID(cls, "<init>", "()V"); //新建物件 jobject obj = env->NewObject(cls, constructor_mid); //... //不在使用jobject物件了 //通知垃圾回收器回收這些物件 //釋放區域性引用 env->DeleteLocalRef(obj); //... } }
4.1.3 弱全域性引用
為了節省記憶體,在記憶體不足時可以是釋放所引用的物件,可以引用一個不常用的物件,如果為NULL,臨時建立,弱全域性引用使用 NewGlobalWeakRef
建立,使用 DeleteGlobalWeakRef
釋放。下面簡稱弱引用。
與全域性引用類似, 弱引用可以跨方法、執行緒使用 。但與全域性引用很重要不同的一點是,弱引用不會阻止GC回收它引用的物件,所以在使用時需要多加小心, 它所引用的物件可能是不存在的或者已經被回收 。
1.建立弱全域性引用
用NewWeakGlobalRef函式對弱全域性引用進行初始化,例如:
extern "C" JNIEXPORT void JNICALL Java_com_richy_cmakedemo_JniTest_weakGlobalRef(JNIEnv *env, jobject instance) { weakGlobalcls = static_cast<jclass>(env->NewWeakGlobalRef(instance)); //... if (JNI_FALSE == env->IsSameObject(weakGlobalcls, NULL)) { //TODO 物件未被回收,可以使用 } else { //TODO 物件被垃圾回收器回收,不能使用,根據業務需求判斷是否要重新新建 } //.... }
2.引用的比較
IsSameObject
來判斷它們兩個是否指向相同的物件。例如:(
env)->IsSameObject(env, obj1, obj2)
如果obj1和obj2指向相同的物件,則返回JNI_TRUE(或者1),否則返回JNI_FALSE(或者0)。
有一個特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null物件。如果obj是一個 區域性或全域性引用 ,使用(*env)->IsSameObject(env, obj, NULL) 或者 obj == NULL 來判斷obj是否指向一個null物件即可。但需要注意的是,IsSameObject用於弱全域性引用與NULL比較時,返回值的意義是不同於區域性引用和全域性引用的。比如:
if(JNI_FALSE == (*env)->IsSameObject(env,weakGlobalcls,NULL)){ //TODO 物件未被回收,可以使用 }else{ //TODO 物件被垃圾回收器回收,不能使用,根據業務需求判斷是否要重新新建 }
ref:
Android NDK 開發(三)JNI 呼叫Java屬性和方法