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"
}
}
复制代码