NDK開發就是先用C/C++開發,而後把C/C++或者彙編代碼編譯成動態連接庫,最後JVM加載庫文件,經過JNI在Java和C/C++之間進行互相調用。通常狀況下,在性能敏感、音視頻和跨平臺等場景,都會涉及NDK開發。本文主要介紹經過Cmake進行NDK開發的一些配置,以及JNI相關知識。html
進行NDK開發,須要進行一些簡單配置,首先在local.properties
中添加SDK和NDK路徑,其次在Android SDK中的SDK Tools安裝CMake和LLDB,而後在gradle.properties
中移除android.useDeprecatedNdk = true
java
ndk.dir=/Users/xxx/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/xxx/Library/Android/sdk
cmake.dir=/Users/xxx/Library/Android/sdk/cmake/3.6.4111459
複製代碼
在模塊級build.gradle
中添加Cmake配置,以下所示:android
android {
......
defaultConfig {
......
externalNativeBuild {
cmake {
// 設置C++編譯器參數
cppFlags "-std=c++11"
// 設置C編譯器參數
cFlags ""
// 設置Cmake參數,在CMakeLists.txt中能夠直接訪問參數
arguments "-DParam=true"
}
}
ndk {
// 指定編譯輸出的庫文件ABI架構
abiFilters "armeabi-v7a"
}
}
externalNativeBuild {
cmake {
// 設置Cmake編譯文件的路徑
path "CMakeLists.txt"
// 設置Cmake版本號
version "3.6.4111459"
}
}
}
複製代碼
下面咱們看一下一個典型的CMakeLists.txt
的內容:ios
# 設置Cmake的最低版本號
cmake_minimum_required(VERSION 3.4.1)
# 日誌輸出
MESSAGE(STATUS "Param = ${Param}")
# 指定頭文件搜索路徑
include_directories("......")
# 基於源文件添加Library
add_library( # Sets the name of the library.
avpractice
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/_onload.cpp)
# 基於靜態庫添加Library
add_library(
libavcodec-lib
STATIC
IMPORTED)
# 設置libavcodec-lib的靜態庫路徑
set_target_properties( # Specifies the target library.
libavcodec-lib
# Specifies the parameter you want to define.
PROPERTIES IMPORTED_LOCATION
# Provides the path to the library you want to import.
${FFMPEG_PATH}/lib/${ANDROID_ABI}/libavcodec.a)
# 尋找NDK提供的庫文件,這裏是EGL
find_library( # Sets the name of the path variable.
egl-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
EGL )
# 指定連接庫,這裏會生層一個libavpractice.so
target_link_libraries( # Specifies the target library.
avpractice
libavcodec-lib
# Links the target library to the log library
# included in the NDK.
${egl-lib})
複製代碼
經過上述的add_library
和target_link_libraries
,咱們能夠同時生成多個動態庫文件。c++
JNI全稱是:Java Native Interface,即鏈接JVM和Native代碼的接口,它容許Java和Native代碼之間互相調用。在Android平臺,Native代碼是指使用C/C++或彙編語言編寫的代碼,編譯後將以動態連接庫(.so)的形式供Java虛擬機加載,並遵守JNI規範互相調用。本質來講,JNI只是Java和C/C++之間的中間層,在組織代碼結構時,通常也是把Java、JNI和跨平臺的C/C++代碼放在不一樣目錄。下面咱們看一些JNI中比較重要的知識點。git
創建Java和Native方法的關聯關係主要有兩種方式:github
javah
生成對應的Native方法名。JNI_OnLoad
中註冊JNI函數表。假設Java層的Native方法以下所示:shell
package com.leon;
public class LeonJNI {
static {
// 加載so
System.loadLibrary("leon");
}
// Native Method
public native String hello();
// Static Native Method
public static native void nihao(String str);
}
複製代碼
那麼經過javah
生成頭文件的命令以下所示(當前目錄是包名路徑的上一級,即com目錄的父目錄):編程
javah -jni com.leon.LeonJNI
複製代碼
生成頭文件中的核心Native方法以下所示:數組
/* * 對應LeonJNI.hello實例方法 * Class: com_leon_LeonJNI * Method: hello * Signature: ()Ljava/lang/String; */
JNIEXPORT jstring JNICALL Java_com_leon_LeonJNI_hello (JNIEnv *, jobject);
/* * 對應LeonJNI.nihao靜態方法 * Class: com_leon_LeonJNI * Method: nihao * Signature: (Ljava/lang/String;)V */
JNIEXPORT void JNICALL Java_com_leon_LeonJNI_nihao (JNIEnv *, jclass, jstring);
複製代碼
當Java層加載動態連接庫時(System.loadLibrary("leon")
),Native層jint JNI_OnLoad(JavaVM *vm, void *reserved)
全局方法首先會被調用,因此這裏是註冊JNI函數表的最佳場所。
假設Java層實現不變,對應的Native層代碼以下所示:
#define PACKAGE_NAME "com/leon/LeonJNI"
#define ARRAY_ELEMENTS_NUM(p) ((int) sizeof(p) / sizeof(p[0]))
//全局引用
jclass g_clazz = nullptr;
// 對應LeonJNI.nihao靜態方法
jstring JNICALL nativeHello(JNIEnv *env, jobject obj) {
......
}
// 對應LeonJNI.nihao靜態方法
void JNICALL nativeNihao(JNIEnv * env , jclass clazz, jstring jstr){
......
}
// 方法映射表
static JNINativeMethod methods[] = {
{"hello", "()Ljava/lang/String;", (void *) nativeHello},
{"nihao", "(Ljava/lang/String;)V", (void *) nativeNihao},
};
// 註冊函數表
static int register_native_methods(JNIEnv *env) {
if (env->RegisterNatives(g_clazz, methods, ARRAY_ELEMENTS_NUM(methods)) < 0){
return JNI_ERR;
}
return JNI_OK;
}
// JVM加載動態庫時,被調用
jint JNI_OnLoad(JavaVM *vm, void *reserved){
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_EVERSION;
}
jclass clazz = env->FindClass(PACKAGE_NAME);
if (clazz == nullptr) {
return JNI_EINVAL;
}
g_clazz = (jclass) env->NewGlobalRef(clazz);
env->DeleteLocalRef(clazz);
int result = register_native_methods(env);
if (result != JNI_OK) {
LOGE("native methods register failed");
}
return JNI_VERSION_1_6;
}
// JVM卸載動態庫時,被調用
void JNI_OnUnload(JavaVM* vm, void* reserved){
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return ;
}
if(g_clazz != nullptr){
env->DeleteGlobalRef(g_clazz);
}
// 其餘清理工做
......
}
複製代碼
JNI_OnLoad
是全局函數,一個動態連接庫只能有一個實現。
從Native調用Java,與Java的反射調用相似,首先要獲取Java類的jclass
對象,而後獲取屬性或者方法的jfieldID
或者jmethodID
。針對成員屬性,經過JNIEnv->Set(Static)XXField
設置屬性值,經過JNIEnv->Get(Static)XXField
獲取屬性值,其中XX表示成員屬性的類型。針對成員方法,經過JNIEnv->Call(Static)YYMethod
調用方法,其中YY表示成員方法的返回值類型。下面咱們來看一個簡單示例。 在上面LeonJNI類中新增了兩個從Native層調用的方法:
package com.leon;
public class LeonJNI {
static {
// 加載so
System.loadLibrary("leon");
}
// Native Method
public native String hello();
// Static Native Method
public static native void nihao(String str);
// 從Native調用的實例方法,必須進行反混淆
public String strToNative(){
return "Test";
}
// 從Native調用的靜態方法,必須進行反混淆
public static int intToNative(){
return 100;
}
}
複製代碼
而後,從Native調用Java層方法的示例以下所示(簡化後的代碼):
//全局引用,com.leon.LeonJNI對應的jclass,從Native層調用Java層靜態方法時,做爲參數使用
jclass g_clazz = nullptr;
// com.leon.LeonJNI對應的對象,從Native層調用Java層實例方法時,表示具體調用哪一個類對象的實例方法
jobject g_obj = nullptr;
// LeonJNI.strToNative對應的jmethodID
jmethodID strMethod = env->GetMethodID(g_clazz, "strToNative", "()Ljava/lang/String;");
// LeonJNI.intToNative對應的jmethodID
jmethodID intMethod = env->GetStaticMethodID(g_clazz, "intToNative", "()I");
// 調用實例方法:LeonJNI.strToNative
jstring strResult = (jstring)env->CallObjectMethod(g_obj,strMethod);
// 調用靜態方法:LeonJNI.intToNative
jint intResult = env->CallStaticIntMethod(g_clazz,intMethod);
複製代碼
上述代碼雖然簡單,但確是從Native調用Java方法的基本流程,關於Java和Native之間的參數傳遞以及處理,接下來會進行更詳細的介紹。
上述從Native層調用Java方法,前提是Native持有JNIEnv指針。在Java線程中,JNIEnv實例保存在線程本地存儲 TLS(Thread Local Storage)中,所以不能在線程間共享JNIEnv指針,若是當前線程的TLS中存有JNIEnv實例,只是沒有指向該實例的指針,能夠經過JavaVM->GetEnv((JavaVM*, void**, jint))
獲取指向當前線程持有的JNIEnv實例的指針。JavaVM是全進程惟一的,能夠被全部線程共享。
還有一種更特殊的狀況:即線程自己沒有JNIEnv實例(例如:經過pthread_create()建立的Native線程),這種狀況下須要調用JavaVM->AttachCurrentThread()
將線程依附於JavaVM以得到JNIEnv實例(Attach到JVM後就被視爲Java線程)。當Native線程退出時,必須配對調用JavaVM->DetachCurrentThread()
以釋放JVM資源,例如:局部引用。
爲了不DetachCurrentThread
沒有配對調用,能夠經過 int pthread_key_create(pthread_key_t* key, void (*destructor)(void*))
建立一個 TLS的pthread_key_t:key,並註冊一個destructor回調函數,它會在線程退出前被調用,所以很適合用於執行相似DetachCurrentThread
的清理工做。此外,還能夠調用pthread_setspecific
函數把JNIEnv指針保存到TLS中,這樣不只能夠隨用隨取,並且當destructor函數被調用時,JNIEnv指針也會做爲參數傳入,方便調用Java層的一些清理方法。示例代碼以下所示:
// 全進程惟一的JavaVM
JavaVM * javaVM;
// TLS key
pthread_key_t threadKey;
// 線程退出時的清理函數
void JNI_ThreadDestroyed(void *value) {
JNIEnv *env = (JNIEnv *) value;
if (env != nullptr) {
javaVM->DetachCurrentThread();
pthread_setspecific(threadKey, nullptr);
}
}
// 獲取JNIEnv指針
JNIEnv* getJNIEnv() {
// 首先嚐試從TLS Key中獲取JNIEnv指針
JNIEnv *env = (JNIEnv *) pthread_getspecific(threadKey);
if (env == nullptr) {
// 而後嘗試從TLS中獲取指向JNIEnv實例的指針
if (JNI_OK != javaVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6)) {
// 最後只能attach到JVM,才能獲取到JNIEnv指針
if (JNI_OK == javaVM->AttachCurrentThread(&env, nullptr)) {
// 把JNIEnv指針保存到TLS中
pthread_setspecific(threadKey, env);
}
}
}
return env;
}
jint JNI_OnLoad(JavaVM *vm, void *) {
javaVM = vm;
// 建立TLS Key,並註冊線程銷燬函數
pthread_key_create(&threadKey, JNI_ThreadDestroyed);
return JNI_VERSION_1_6;
}
複製代碼
在JNI中,當咱們使用GetFieldID、GetMethodID等函數操做Java對象時,須要表示成員屬性的類型,或者成員函數的方法簽名,JNI以簡寫的形式組織這些類型。
對於成員屬性,直接以Java類型的簡寫表示便可。 例如:
示例:
jfieldID name = (*env)->GetFieldID(objectClass,"name","Ljava/lang/String;");
jfieldID age = (*env)->GetFieldID(objectClass,"age","I");
複製代碼
對於成員函數,以(*)+
形式表示函數的方法簽名。()
中的字符串表示函數參數,括號外則表示返回值。 例如:
()V
表示void method();(II)V
表示 void method(int, int);(Ljava/lang/String;Ljava/lang/String;)I
表示 int method(String,String)示例:
jmethodID ageId = (*env)->GetMethodID(env, objectClass,"getAge","(Ljava/lang/String;Ljava/lang/String;)I");
複製代碼
JNI中的類型簡寫以下所示:
Java類型 | 類型簡寫 |
---|---|
Boolean | Z |
Char | C |
Byte | B |
Short | S |
Int | I |
Long | J |
Float | F |
Double | D |
Void | V |
Object對象 | 以L 開頭,以; 結尾,中間用/ 分割的包名和類名。 |
數組對象 | 以[ 開頭,加上數組類型的簡寫。例如:[I 表示 int []; |
在JNI的調用中,共涉及到Java層類型、JNI層類型和C/C++層類型(其實,JNI類型是基於C/C++類型經過typedef
定義的別名,這裏拆分出來是爲了更加清晰,便於理解)。那麼這幾種類型之間是如何映射的,其實jni.h裏面給出了JNI層類型的定義。 總體的類型映射以下表所示:
Java類型 | JNI類型 | C/C++類型 |
---|---|---|
boolean | jboolean | unsigned char (8 bits) |
char | jchar | unsigned short (16 bits) |
byte | jbyte | signed char (8 bits) |
short | jshort | signed short (16 bits) |
int | jint | signed int (32 bits) |
long | jlong | signed long long(64 bits) |
float | jfloat | float (32 bits) |
double | jdouble | double (32 bits) |
Object | jobject | void*(C)或者 _jobject指針(C++) |
Class | jclass | jobject的別名(C)或者 _jclass指針(C++) |
String | jstring | jobject的別名(C)或者 _jstring指針(C++) |
Object[] | jobjectArray | jarray的別名(C)或者 _jobjectArray指針(C++) |
boolean[] | jbooleanArray | jarray的別名(C)或者 _jbooleanArray指針(C++) |
char[] | jcharArray | jarray的別名(C)或者 _jcharArray指針(C++) |
byte[] | jbyteArray | jarray的別名(C)或者 _jbyteArray指針(C++) |
short[] | jshortArray | jarray的別名(C)或者 _jshortArray指(C++) |
int[] | jintArray | jarray的別名(C)或者 _jintArray指針(C++) |
long[] | jlongArray | jarray的別名(C)或者 _jlongArray指針(C++) |
float[] | jfloatArray | jarray的別名(C)或者 _jfloatArray指(C++) |
double[] | jdoubleArray | jarray的別名(C)或者_jdoubleArray指針(C++) |
衆所周知,Java包括2種數據類型:基本類型和引用類型,JNI對基本類型的處理比較簡單:Java層的基本類型和C/C++層的基本類型是一一對應,能夠直接相互轉換,jni.h
中的定義以下所示:
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
複製代碼
而對於引用類型,若是JNI是用C語言編寫的,那麼其定義以下所示,即全部引用類型都是jobject類型:
typedef void* jobject;
typedef jobject jclass;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jobjectArray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jobject jthrowable;
typedef jobject jweak;
複製代碼
若是JNI是用C++語言編寫的,那麼其定義以下所示:
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray* jbyteArray;
typedef _jcharArray* jcharArray;
typedef _jshortArray* jshortArray;
typedef _jintArray* jintArray;
typedef _jlongArray* jlongArray;
typedef _jfloatArray* jfloatArray;
typedef _jdoubleArray* jdoubleArray;
typedef _jthrowable* jthrowable;
typedef _jobject* jweak;
複製代碼
JNI利用C++的特性,創建了一個引用類型集合,集合中全部類型都是jobject的子類,這些子類和Java中的引用類型相對應。例如:jstring表示字符串、jclass表示class字節碼對象、jarray表示數組,另外jarray派生了9個子類,分別對應Java中的8種基本數據類型(jintArray、jbooleanArray、jcharArray等)和對象類型(jobjectArray)。 因此,JNI整個引用類型的繼承關係以下圖所示:
總的來講,Java層類型映射到JNI層的類型是固定的,可是JNI層類型在C和C++平臺具備不一樣的解釋。
上面介紹了Java層類型、JNI層類型和C/C++層類型三種類型之間的映射關係。下面咱們看下Java層的基本類型和引用類型,在Native層的具體操做。
對於基本類型,不論是Java->Native,仍是Native->Java,均可以在Java和C/C++之間直接轉換,須要注意的是Java層的long是8字節,對應到C/C++是long long類型。
Java的String和C++的string是不對等的,因此必須進行轉換處理。
//把UTF-8編碼格式的char*轉換爲jstring
jstring (*NewStringUTF)(JNIEnv*, const char*);
//獲取jstring的長度
size (*GetStringUTFLength)(JNIEnv*, jstring);
//把jstring轉換成爲UTF-8格式的char*
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
//釋放指向UTF-8格式的char*的指針
void (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);
//示例:
#include <iostream>
JNIEXPORT jstring JNICALL Java_Main_getStr(JNIEnv *env, jobject obj, jstring arg) {
const char* str;
//把jstring轉換爲UTF-8格式的char *
str = (*env)->GetStringUTFChars(arg, false);
if(str == NULL) {
return NULL;
}
std::cout << str << std::endl;
//顯示釋放jstring
(*env)->ReleaseStringUTFChars(arg, str);
//建立jstring,返回到java層
jstring rtstr = (*env)->NewStringUTF("Hello String");
return rtstr;
}
複製代碼
在使用完轉換後的char *
以後,須要顯示調用 ReleaseStringUTFChars方法,讓JVM釋放轉換成UTF-8的string的對象空間,若是不顯示調用,JVM會一直保存該對象,不會被GC回收,所以會致使內存泄漏。
在JNI中,除了String以外(jstring),其餘的對象類型都映射爲jobject。JNI提供了在Native層操做Java層對象的能力: 1.首先經過FindClass
或者GetObjectClass
得到對應的jclass對象。
//根據類名獲取對應的jclass對象
jclass (*FindClass)(JNIEnv*, const char*);
//根據已有的jobject對象獲取對應的jclass對象
jclass (*GetObjectClass)(JNIEnv*, jobject);
//示例:
//獲取User對應的jclass對象
jclass clazz = (*env)->FindClass("com.leon.User") ;
//獲取User對應的jclass對象,jobject_user標識jobject對象
jclass clazz = (*env)->GetObjectClass (env , jobject_user);
複製代碼
2.而後經過GetFieldID/GetStaticFieldID
得到成員屬性IDjfieldID
,或者經過GetMethodID/GetStaticMethodID
得到成員函數IDjmethodID
。
//得到Java類的實例成員屬性
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
//獲取Java類的靜態成員屬性
jfieldID (*GetStaticFieldID)(JNIEnv*, jclass, const char*,
const char*);
//獲取Java類的實例成員函數
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
//獲取Java類的靜態成員函數
jmethodID (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
//第一個參數固定是JNIENV,第二個參數jclass表示在哪一個類上操做,第三個參數表示對應的成員屬性或者成員函數的名字,第四個參數表示對應的成員屬性的類型或者成員函數的方法簽名。
//示例:
jmethodID getAgeId = (*env)->GetMethodID(env, jclass,"getAge","()I");
複製代碼
3.最後對獲取的jfieldID
和jmethodID
進行操做。針對成員屬性,主要是獲取和設置屬性值,而屬性又可分爲實例屬性和靜態屬性,所以操做成員屬性的函數原型以下所示:
//獲取實例屬性的值
// 實例屬性是基本類型
JNIType (*Get<PrimitiveType>Field)(JNIEnv*, jobject, jfieldID)
// 實例屬性是對象類型
jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);
//設置實例屬性的值
// 實例屬性是基本類型
void (*Set<PrimitiveType>Field)(JNIEnv*, jobject, jfieldID, JNIType)
// 實例屬性是對象類型
void (*SetObjectField)(JNIEnv*, jobject, jfieldID, jobject);
//獲取靜態屬性的值
// 靜態屬性是基本類型
JNIType (*GetStatic<PrimitiveType>Field)(JNIEnv*, jclass, jfieldID)
// 靜態屬性是對象類型
jobject (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);
//設置靜態屬性的值
// 靜態屬性是基本類型
void (*SetStatic<PrimitiveType>Field)(JNIEnv*, jclass, jfieldID, JNIType)
// 靜態屬性是對象類型
void (*SetStaticObjectField)(JNIEnv*, jclass, jfieldID, jobject);
複製代碼
其中,PrimitiveType
表示Java基本類型,JNIType
表示對應的JNI基本類型。 針對成員方法,主要是調用成員方法,而成員方法又分爲實例方法和靜態方法。所以操做成員方法的函數原型以下所示:
// 調用實例方法
// 實例方法的返回值是對象類型
jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
// 實例方法無返回值
void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
// 實例方法的返回值是基本類型
JNIType (*Call<PrimitiveType>Method)(JNIEnv*, jobject, jmethodID, ...);
// 調用靜態方法
// 靜態方法的返回值是對象類型
jobject CallStaticObjectMethod (jclass cl0, jmethodID meth1, ...) // 靜態方法無返回值 void CallStaticVoidMethod (jclass cl0, jmethodID meth1, ...) // 靜態方法的返回值是基本類型 JNIType (*CallStatic<PrimitiveType>Method)(JNIEnv*, jobject, jmethodID, ...);
複製代碼
在JNI中,也能夠建立一個Java對象,主要經過如下方法:
//jclass表示要建立的類,jmethodID表示用哪一個構造函數建立該類的實例,後面的則爲構造函數的參數
jobject (*NewObject)(JNIEnv*, jclass, jmethodID, ...);
//示例
jclass strClass = (*env)->FindClass(env,"Ljava/lang/String;");
jmethodID ctorID = (*env)->GetMethodID(env,strClass, "<init>", "(Ljava/lang/String;)V");
jobject str = (*env)->NewObject(env,strClass,ctorID,"name");
複製代碼
經過上面的類型介紹可知,JNI共有9種數組類型:jobjectArray
和8種基本類型數組,簡單表示爲j<PrimitiveType>Array
。對於jobjectArray
,JNI只提供了GetObjectArrayElement
和SetObjectArrayElement
方法容許每次操做數組中的一個對象。對於基本類型數組j<PrimitiveType>Array
,JNI提供了2種訪問方式。
JNI提供了以下原型的方法,把Java數組映射爲C數組
JNIType *Get<PrimitiveType>ArrayElements(JNIEnv *env, JNIArrayType array, jboolean *isCopy)
複製代碼
其中,JNIType
表示jint、jlong等基本類型,JNIArrayType
表示jintArray、jlongArray等對應的JNI數組類型。
上述方法會返回指向Java數組的堆地址或新申請副本的地址(能夠傳遞非NULL的isCopy 指針來確認返回值是否爲副本),若是指針指向Java數組的堆地址而非副本,在 Release<PrimitiveType>ArrayElements
以前,此Java數組都沒法被GC回收,因此 Get<PrimitiveType>ArrayElements
和Release<PrimitiveType>ArrayElements
必須配對調用以免內存泄漏。另外Get<PrimitiveType>ArrayElements
可能因內存不足建立副本失敗而返回NULL,因此應該先對返回值判空後再使用。
Release<PrimitiveType>ArrayElements
方法原型以下:
void Release<PrimitiveType>ArrayElements(JNIEnv *env, JNIArrayType array, JNIType *jniArray, jint mode);
複製代碼
最後一個參數mode僅對jniArray爲副本時有效,能夠用於避免一些非必要的副本拷貝,共有如下三種取值:
通常來講,mode爲0是最合適的選擇,這樣無論Get<PrimitiveType>ArrayElements
返回值是不是副本,都不會發生數據不一致和內存泄漏問題。但也有一些場景爲了性能等因素考慮會使用非零值,好比:對於一個尺寸很大的數組,若是獲取指針 以後經過isCopy確認是副本,且以後沒有修改過內容,那麼徹底可使用JNI_ABORT避免回寫以提升性能;另外一種場景是Native修改數組和Java讀取數組在交替進行(如多線程環境),若是經過isCopy確認獲取的數組是副本,則能夠經過JNI_COMMIT模式,可是JNI_COMMIT不會釋放副本,因此最終還須要使用其餘mode,再調用Release<PrimitiveType>ArrayElements
以免副本泄漏。
一種常見的錯誤用法:當isCopy爲false時,沒有調用對應的
Release<PrimitiveType>ArrayElements
。此時雖然未建立副本,可是Java數組的堆內存被引用後會阻止GC回收,所以也必須配對調用Release方法。
針對JVM基本類型數組,還能夠進行塊拷貝,包括:從JVM拷貝到Native和從Native拷貝到JVM。
從JVM拷貝到Native的函數原型以下所示:表示把數據從JVM的array數組拷貝到Native層的buf數組。
Get<PrimitiveType>ArrayRegion(JNIEnv *env, JNIArrayType array,jsize start, jsize len, JNIType * buf)
複製代碼
從Native拷貝到JVM的函數原型以下所示:表示把數據從Native層的buf數組拷貝到JVM的array數組。
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, JNIArrayType array, jsize start, jsize len, const JNIType * buf)
複製代碼
其中,JNIType
表示jint、jlong等基本類型,JNIArrayType
表示jintArray、jlongArray等對應的JNI數組類型。
相比於前一種數組操做方式,塊拷貝有如下優勢:
JNI規範中定義了三種引用:全局引用(Global Reference),局部引用(Local Reference)和弱全局引用(Weak Global Reference)。無論哪一種引用,持有的都是jobject及其子類對象(包括 jclass, jstring, jarray等,但不包括指針類型、jfieldID和jmethodID)。
引用和被引用對象是兩個不一樣的對象,只有先釋放了引用對象才能釋放被引用對象。
每一個傳給Native方法的對象參數(jobject及其子類,包括 jclass, jstring, jarray等)和幾乎全部JNI函數返回的對象都是局部引用。這意味着它們只在當前線程的當前Native方法內有效,一旦該方法返回則失效(哪怕被引用的對象仍然存在)。因此正常狀況下,咱們無須手動調用DeleteLocalRef
釋放局部引用,除非如下幾種狀況:
上述對象是指jobject及其子類,包括jclass、jstring、jarray,不包括GetStringUTFChars和GetByteArrayElements這類函數的原始數據指針返回值,也不包括jfieldID和jmethodID ,在Android下這二者在類加載以後就一直有效。
Native方法內建立的jobject及其子類對象(包括jclass、jstring、jarray等,但不包括指針類型、jfieldID和jmethodID),默認都是局部引用。
全局引用
的生存期爲建立(NewGlobalRef)後,直到咱們顯式釋放它(DeleteGlobalRef)。 弱全局引用
的生存期爲建立(NewWeakGlobalRef)後,直到咱們顯式釋放(DeleteWeakGlobalRef)它或者JVM認爲應該回收它的時候(好比:內存緊張),進行回收釋放。
(弱)全局引用能夠跨線程跨方法使用,由於經過NewGlobalRef
或者NewWeakGlobalRef
方法建立後會一直有效,直到調用DeleteGlobalRef
或者DeleteWeakGlobalRef
方法手動釋放。這個特性經常使用於緩存一些獲取起來較耗時的對象,好比:經過FindClass
獲取的jclass,Java層傳下來的jobject等,這些對象均可以經過全局引用緩存起來,供後續使用。
比較兩個引用是否指向同一個對象可使用IsSameObject函數
jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2);
複製代碼
JNI中的NULL指向JVM中的null對象,IsSameObject用於弱全局引用(WeakGlobalRef)與NULL比較時,返回值表示其引用的對象是否已經回收(JNI_TRUE表明已回收,該弱引用已無效)。
JNI把Java中的對象當作一個C指針傳遞到Native方法,這個指針指向JVM中的內部數據結構,而內部數據結構在內存中的存儲方式對外是不可見的。因此,Native方法必須經過在
JNIEnv
中選擇適當的JNI函數來操做JVM中的對象。
經過
JNIEnv
建立的對象都受JVM管理,雖然這些對象在在Native層建立(經過Jni接口),可是能夠經過返回值等多種方式引入到Java層,這也間接說明了這些對象分配在Java Heap中。
NDK開發中總會遇到一些奇奇怪怪的問題,這裏列舉一些典型問題。
假如遇到FindClass失敗問題,首先要排除一些簡單緣由:
java/lang/String
,檢查是否用/
分割包名和類名,此時不須要添加L
和;
,若是是內部類,那麼使用$
而不是.
去標識。若是你排除了以上緣由,仍是沒法找到對應類,那可能就是多線程問題了。 通常狀況下,從Java層調用到Native層時,會攜帶棧幀信息(stack frames),其中包含加載當前應用類的ClassLoader
,FindClass
會依賴該ClassLoader
去查找類(此時,通常是負責加載APP類的PathClassLoader)。 可是若是在Native層經過pthread_create
建立線程,而且經過AttachCurrentThread
關聯到JVM,那麼此時沒有任何關於App的棧幀信息,因此FindClass
會依賴系統類加載器去查找類(此時,通常是負責加載系統類的BootClassLoader)。所以,加載全部的APP類都會失敗,可是能夠加載系統類,例如:android/graphics/Bitmap
。
有如下幾種解決方案:
JNI_OnLoad
(Java層調用System.loadLibrary時,會被觸發)中,經過FindClass
找出全部須要的jclass
,而後經過全局引用緩存起來,後面須要時直接使用便可。loadClass
的MethodID,而後經過調用PathClassLoader.loadClass
方法直接加載指定類。下面分別看一下方案1和方案2的簡單示例:
jclass cacheClazz = nullptr;
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *pEnv = nullptr;
if(vm->GetEnv((void **) &pEnv, JNI_VERSION_1_6) != JNI_OK){
return JNI_ERR;
}
jclass clazz = env->FindClass("com/leon/BitmapParam");
if (clazz == nullptr) {
return JNI_ERR;
}
// 建立並緩存全局引用
cacheClazz = (jclass) env->NewGlobalRef(clazz);
// 刪除局部引用
env->DeleteLocalRef(clazz);
return JNI_VERSION_1_6;
}
複製代碼
而後能夠在任何Native線程,經過上述緩存的cacheClazz,去獲取jmethodID
和jfieldID
,而後實現對Java對象的訪問。
// 緩存的classloader
jobject jobject_classLoader = nullptr
// 緩存的loadClass的methodID
jmethodID loadClass_methodID = nullptr
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *pEnv = nullptr;
if(vm->GetEnv((void **) &pEnv, JNI_VERSION_1_6) != JNI_OK){
return JNI_ERR;
}
// jclass point to Test.java,這裏能夠是App的任意類
jclass jclass_test = env->FindClass("com/ltlovezh/avpractice/render/Test");
// jclass point to Class.java
jclass jclass_class = env->GetObjectClass(jclass_test);
jmethodID getClassLoader_methodID = env->GetMethodID(jclass_class, "getClassLoader", "()Ljava/lang/ClassLoader;");
jobject local_jobject_classLoader = env->CallObjectMethod(jclass_test, getClassLoader_methodID);
// 建立全局引用
jobject_classLoader = env->NewGlobalRef(local_jobject_classLoader);
jclass jclass_classLoader = env->FindClass("java/lang/ClassLoader");
loadClass_methodID = env->GetMethodID(jclass_classLoader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
// 刪除局部引用
env->DeleteLocalRef(jclass_test);
env->DeleteLocalRef(jclass_class);
env->DeleteLocalRef(local_jobject_classLoader);
env->DeleteLocalRef(jclass_classLoader);
return JNI_VERSION_1_6;
}
// 經過緩存的ClassLoader直接Find Class
jclass findClass(JNIEnv *pEnv, const char* name) {
return static_cast<jclass>(pEnv->CallObjectMethod(jobject_classLoader, loadClass_methodID, pEnv->NewStringUTF(name)));
}
複製代碼
上述在JNI_OnLoad
中緩存了ClassLoader
和loadClass的jmethodID,在須要時能夠直接加載指定類,獲取對應的jclass。
曾經遇到過使用cmake3.10,致使C++代碼沒法關聯跳轉的問題,後來對cmake降級處理就OK了。具體步驟以下:
在local.properties
中指定cmake路徑:
cmake.dir=/Users/xxx/Library/Android/sdk/cmake/3.6.4111459
複製代碼
在模塊級build.gradle
中指定cmake版本:
externalNativeBuild {
cmake {
path "CMakeLists.txt"
version "3.6.4111459"
}
}
複製代碼