NDK开发实践

NDK开发就是先用C/C++开发,而后把C/C++或者汇编代码编译成动态连接库,最后JVM加载库文件,经过JNI在Java和C/C++之间进行互相调用。通常状况下,在性能敏感、音视频和跨平台等场景,都会涉及NDK开发。本文主要介绍经过Cmake进行NDK开发的一些配置,以及JNI相关知识。html

基于Cmake进行NDK开发

进行NDK开发,须要进行一些简单配置,首先在local.properties中添加SDK和NDK路径,其次在Android SDK中的SDK Tools安装CMake和LLDB,而后在gradle.properties中移除android.useDeprecatedNdk = truejava

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_librarytarget_link_libraries,咱们能够同时生成多个动态库文件。c++

JNI简介

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的互相调用

Java调用Native

创建Java和Native方法的关联关系主要有两种方式:github

  • 静态关联:根据Java方法和Native方法的命名规范进行绑定,通常根据Java层Native方法名经过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

从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之间的参数传递以及处理,接下来会进行更详细的介绍。

获取JNIEnv指针

上述从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中Java类型的简写

在JNI中,当咱们使用GetFieldID、GetMethodID等函数操做Java对象时,须要表示成员属性的类型,或者成员函数的方法签名,JNI以简写的形式组织这些类型。

对于成员属性,直接以Java类型的简写表示便可。 例如:

  • "I"表示该成员变量是Int类型;
  • "Ljava/lang/String;"表示该成员变量是String类型。

示例:

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中的参数传递和操做

在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整个引用类型的继承关系以下图所示:

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.最后对获取的jfieldIDjmethodID进行操做。针对成员属性,主要是获取和设置属性值,而属性又可分为实例属性和静态属性,所以操做成员属性的函数原型以下所示:

//获取实例属性的值
// 实例属性是基本类型
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只提供了GetObjectArrayElementSetObjectArrayElement方法容许每次操做数组中的一个对象。对于基本类型数组j<PrimitiveType>Array,JNI提供了2种访问方式。

把基本类型的Java数组映射为C数组

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>ArrayElementsRelease<PrimitiveType>ArrayElements必须配对调用以免内存泄漏。另外Get<PrimitiveType>ArrayElements可能因内存不足建立副本失败而返回NULL,因此应该先对返回值判空后再使用。

Release<PrimitiveType>ArrayElements方法原型以下:

void Release<PrimitiveType>ArrayElements(JNIEnv *env, JNIArrayType array, JNIType *jniArray, jint mode);
复制代码

最后一个参数mode仅对jniArray为副本时有效,能够用于避免一些非必要的副本拷贝,共有如下三种取值:

  1. 0,将jniArray数组内容回写到Java数组,并释放jniArray占用的内存。
  2. JNI_COMMIT,将jniArray数组内容回写到Java数组,但不释放jniArray占用的内存。
  3. JNI_ABORT,不回写jniArray数组内容到Java数组,仅仅释放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引用

JNI规范中定义了三种引用:全局引用(Global Reference),局部引用(Local Reference)和弱全局引用(Weak Global Reference)。无论哪一种引用,持有的都是jobject及其子类对象(包括 jclass, jstring, jarray等,但不包括指针类型、jfieldID和jmethodID)。

引用和被引用对象是两个不一样的对象,只有先释放了引用对象才能释放被引用对象。

局部引用

每一个传给Native方法的对象参数(jobject及其子类,包括 jclass, jstring, jarray等)和几乎全部JNI函数返回的对象都是局部引用。这意味着它们只在当前线程的当前Native方法内有效,一旦该方法返回则失效(哪怕被引用的对象仍然存在)。因此正常状况下,咱们无须手动调用DeleteLocalRef释放局部引用,除非如下几种状况:

  1. Native方法内建立大量的局部引用,例如在循环中反复建立,由于JVM保存局部引用的空间是有限的 (Android为512),一旦循环中建立的引用数超出限制就会致使异常:ReferenceTable overflow (max=512);
  2. 经过AttachCurrentThread()依附到JVM的线程内全部局部引用均不会被自动释放,直到调用DetachCurrentThread()才会统一释放,为避免线程中累积过多局部引用,建议及时手动释放。
  3. Native方法内,局部引用引用了一个很是大的对象,用完后还要进行较长时间的其它运算才能返回,局部引用会阻止该对象被GC。为下降OOM风险,用完后应当及时手动释放。

上述对象是指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开发中总会遇到一些奇奇怪怪的问题,这里列举一些典型问题。

Native线程FindClass失败

假如遇到FindClass失败问题,首先要排除一些简单缘由:

  1. 检查包名、类名是否拼写错误,例如:加载String时,应当是java/lang/String,检查是否用/分割包名和类名,此时不须要添加L;,若是是内部类,那么使用$而不是.去标识。
  2. 检查对应的Java类,是否进行了反混淆,若是你的类/方法/属性仅仅从Native层访问,那就八九不离十是这个缘由了。

若是你排除了以上缘由,仍是没法找到对应类,那可能就是多线程问题了。 通常状况下,从Java层调用到Native层时,会携带栈帧信息(stack frames),其中包含加载当前应用类的ClassLoaderFindClass会依赖该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方法直接加载指定类。
  3. 把须要的Class实例经过参数传递到Native层函数。

下面分别看一下方案1和方案2的简单示例:

方案1:缓存jclass

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,去获取jmethodIDjfieldID,而后实现对Java对象的访问。

方案2:缓存ClassLoader

// 缓存的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。

C++代码没法关联

曾经遇到过使用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"
    }
}
复制代码

参考文档

  1. Android NDK 开发教程
  2. JNI FindClass Error in Native Thread
  3. JNI官方规范
  4. Google JNI tips
相关文章
相关标签/搜索