@ltlovezh
2019-03-11T04:17:35.000000Z
字数 20685
阅读 1487
NDK
NDK开发就是把C/C++或者汇编代码编译成动态链接库,然后JVM加载库文件,通过JNI在Java和C/C++之间进行互相调用。一般情况下,在性能敏感、音视频和跨平台等场景,都会涉及NDK开发。本文主要介绍通过Cmake进行NDK开发的一些配置,以及JNI相关知识。
进行NDK开发,需要进行一些简单配置,首先在local.properties中添加SDK和NDK路径,其次在Android SDK中的SDK Tools安装CMake和LLDB,然后在gradle.properties中移除android.useDeprecatedNdk = true
ndk.dir=/Users/xxx/Library/Android/sdk/ndk-bundlesdk.dir=/Users/xxx/Library/Android/sdkcmake.dir=/Users/xxx/Library/Android/sdk/cmake/3.6.4111459
在模块级build.gradle中添加Cmake配置,如下所示:
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的内容:
# 设置Cmake的最低版本号cmake_minimum_required(VERSION 3.4.1)# 日志输出MESSAGE(STATUS "Param = ${Param}")# 指定头文件搜索路径include_directories("......")# 基于源文件添加Libraryadd_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)# 基于静态库添加Libraryadd_library(libavcodec-libSTATICIMPORTED)# 设置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提供的库文件,这里是EGLfind_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.sotarget_link_libraries( # Specifies the target library.avpracticelibavcodec-lib# Links the target library to the log library# included in the NDK.${egl-lib})
通过上述的add_library和target_link_libraries,我们可以同时生成多个动态库文件。
JNI全称是:Java Native Interface,即连接JVM和Native代码的接口,它允许Java和Native代码之间互相调用。在Android平台,Native代码是指使用C/C++或汇编语言编写的代码,编译后将以动态链接库(.so)的形式供Java虚拟机加载,并遵照JNI规范互相调用。本质来说,JNI只是Java和C/C++之间的中间层,在组织代码结构时,一般也是把Java、JNI和跨平台的C/C++代码放在不同目录。下面我们看一些JNI中比较重要的知识点。
建立Java和Native方法的关联关系主要有两种方式:
javah生成对应的Native方法名。JNI_OnLoad中注册JNI函数表。假设Java层的Native方法如下所示:
package com.leon;public class LeonJNI {static {// 加载soSystem.loadLibrary("leon");}// Native Methodpublic native String hello();// Static Native Methodpublic 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 {// 加载soSystem.loadLibrary("leon");}// Native Methodpublic native String hello();// Static Native Methodpublic 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对应的jmethodIDjmethodID strMethod = env->GetMethodID(g_clazz, "strToNative", "()Ljava/lang/String;");// LeonJNI.intToNative对应的jmethodIDjmethodID intMethod = env->GetStaticMethodID(g_clazz, "intToNative", "()I");// 调用实例方法:LeonJNI.strToNativejstring strResult = (jstring)env->CallObjectMethod(g_obj,strMethod);// 调用静态方法:LeonJNI.intToNativejint 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层的一些清理方法。示例代码如下所示:
// 全进程唯一的JavaVMJavaVM * javaVM;// TLS keypthread_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 | L |
| 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*转换为jstringjstring (*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数组类型。
相比于前一种数组操作方式,块拷贝有以下优点:
1. 只需要一次JNI调用,减少开销。
2. 无需创建副本或引用JVM数组内存(即:不影响GC)
3. 降低编程出错的风险——不会因忘记调用Release函数而引起内存泄漏。
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。
有以下几种解决方案:
1. 在JNI_OnLoad(Java层调用System.loadLibrary时,会被触发)中,通过FindClass找出所有需要的jclass,然后通过全局引用缓存起来,后面需要时直接使用即可。
2. 在Native层缓存App类加载器对象和loadClass的MethodID,然后通过调用PathClassLoader.loadClass方法直接加载指定类。
2. 把需要的Class实例通过参数传递到Native层函数。
下面分别看一下方案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对象的访问。
// 缓存的classloaderjobject jobject_classLoader = nullptr// 缓存的loadClass的methodIDjmethodID loadClass_methodID = nullptrjint 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.javajclass 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 Classjclass 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"}}