阅读 96

NDK开发实践

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

基于Cmake进行NDK开发

进行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_librarytarget_link_libraries,我们可以同时生成多个动态库文件。

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中比较重要的知识点。

Java和Native的互相调用

Java调用Native

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

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

从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
关注下面的标签,发现更多相似文章
评论