@ltlovezh
2019-03-11T12:17:35.000000Z
字数 20685
阅读 1200
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-bundle
sdk.dir=/Users/xxx/Library/Android/sdk
cmake.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("......")
# 基于源文件添加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
,我们可以同时生成多个动态库文件。
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 {
// 加载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 | 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*转换为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数组类型。
相比于前一种数组操作方式,块拷贝有以下优点:
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对象的访问。
// 缓存的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"
}
}