JNI定义的数据类型
JNI 中定义的数据类型和 Java 中定义的数据类型是一一对应的关系。JNI 中定义的数据类 型分为基本数据类型和引用类型。基本数据类型见下表:
JNI 类型 | Java 类型 | 描述 |
---|---|---|
void | void | 无类型 |
jboolean | boolean | 布尔型 |
jbyte | byte | 字节型 |
jchar | char | 字符型 |
jshort | short | 短整型 |
jint | int | 整型 |
jlong | long | 长整型 |
jfloat | float | 浮点型 |
jdouble | bouble | 双精度浮点型 |
引用类型:
JNI 类型 | Java 类型 | 描述 |
---|---|---|
jstring | String | 字符串对象 |
jobjectArray | Object[] | 对象数组 |
jbooleanArray | boolean[] | 布尔型数组 |
jbyteArray | byte[] | |
jcharArray | char[] | |
jshortArray | sort[] | |
jintArray | int[] | |
jlongArray | long[] | |
jfloatArray | float[] | |
jdoubleArra | bouble[] | |
jthrowable | Throwable |
除了以上的类型之外,还有一点需要注意, Java 语言的中的数据类型的签名如下表所示:
Java 类型 | signature |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | void |
String | Ljava/lang/String; |
Object | Ljava/lang/Object; |
Gradle中配置 cmak
在 gradle 的配置文件中配置 cmake 的目的是为了让 gradle 在构建 APK 时能够自动的去 构建 C/C++代码,也就是说要在 gradle 进行配置,以便让 Android Studio 能够识别 C/C++ 代码.要将下面的代码添加到 module 的 build.gradle 文件中.
android { ... defaultConfig { ... // This block is different from the one you use to link Gradle // to your CMake build script. externalNativeBuild { cmake { ... // Use the following syntax when passing arguments to variables: // arguments "-DVAR_NAME=ARGUMENT". arguments "-DANDROID_ARM_NEON=TRUE", // If you're passing multiple arguments to a variable, pass them together: // arguments "-DVAR_NAME=ARG_1 ARG_2" // The following line passes 'rtti' and 'exceptions' to 'ANDROID_CPP_FEATURES'. "-DANDROID_CPP_FEATURES=rtti exceptions" // add filter to api adbFilters "armeabi-v7a", "armeabi-v8a", "x86", "x86-64" } } } buildTypes {...} // Use this block to link Gradle to your CMake build script. externalNativeBuild { cmake { path file('../CmakeLists.txt') } } }
Cmake语法
从 Android studio2.2 开始支持Cmake编译 C/C++代码,而且目前最新版的 Android studio,使用 cmake 作为默认的构建工具.接下来我们看一下最简单的CmakeLists.txt.
cmake_minimum_required(VERSION 3.4.1) add_library( # 生成的so库名称,此处生成的so文件名称是libnative-lib.so native-lib # SHARED是动态库,会被动态链接,在运行时被加载 # STATIC:静态库,是目标文件的归档文件,在链接其它目标的时候使用 # MODULE:模块库,是不会被链接到其它目标中的插件 SHARED # 资源路径是相对路径,相对于本CMakeLists.txt所在目录 src/main/cpp/native-lib.cpp ) # 从系统查找依赖库 find_library( # android系统每个类型的库会存放一个特定的位置,而log库存放在log-lib中 log-lib # android系统在c环境下打log到logcat的库 log ) # 配置库的链接(依赖关系) target_link_libraries( # 目标库 native-lib # 依赖于 ${log-lib} )
如果想在CmakeList s.txt 中添加另一个 so 库的依赖, CmakeListts.txt 如下所示:
cmake_minimum_required(VERSION 3.4.1) add_library( pre-build-lib STATIC src/main/cpp/pre-build-lib ) add_library( native-lib SHARED src/main/cpp/native-lib.cpp src/main/cpp/native-lib2.cpp) # 从系统查找依赖库 find_library( # android系统每个类型的库会存放一个特定的位置,而log库存放在log-lib中 log-lib # android系统在c环境下打log到logcat的库 log ) # 配置库的链接(依赖关系) target_link_libraries( # 目标库 native-lib # 依赖于 ${log-lib} pre-build-lib )
JNI注册的方式
java 要想调用 C/C++代码,java 必须找到相应的 C/C++代码,所以 C/C++代码需要进行注 册,以便 java 代码能够找到相应的 C/C++代码.JNI 支持的注册方式有两种:静态注册方式 和动态注册方式.先看静态的注册方式:
extern "C" JNIEXPORT void JNICALL Java_com_ndk_lingxiao_ndkproject_Hello_callJavaStaticMethod(JNIEnv *env, jclass thiz) { jclass clazz = NULL; jmethodID method_id = NULL; jstring str_log = NULL; clazz = env->FindClass("com/pacakge/Hello"); if (clazz == NULL){ return; } method_id = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;)V"); if (method_id == NULL){ return; } str_log = env->NewStringUTF("c++ 调用java的静态方法"); env->CallStaticVoidMethod(clazz,method_id,str_log); env->DeleteLocalRef(clazz); env->DeleteLocalRef(str_log); return ; }
在静态注册的方式的函数定义部分是比较固定的写法,我们以此来解释一下:
- extern “C”:表示C++代码能够调用其他 C 代码,主要用来兼容 C 语言.
JNIEXPORT: 这个宏的定义如下,这个宏表示这函数对外是可见的,以便于 java 代码能 够找到这个函数.
#define JNIEXPORT __attribute__ ((visibility ("default")))
- JavacomndklingxiaondkprojectHellocallJavaStaticMethod:函数的名称,这个 函数名是有固定的格式的,格式为:
Java_类的全路径名_方法名(在类的全路径名中要将"/"装换成"_", 这个类定义这个 native 方法所在的类)
- JNIEnv env*: JNI调用的运行环境的指针,每个函数的第一个参数都是一个 JNIEnv*类 型.
- jclass thiz: 如果注册的类是一个非静态类,这个thiz 指的的是调用这个方法的类实 例化后的对象.如果注册的类是一个静态类,这个 thiz 指的是就是这个类.
接下来我们看动态注册的方式,在动态注册的方式有又分为两种形式:C 形式 和 C++形式,接 下来我们会分别对这两种形式作介绍. 先看 C 语言版本的动态注册方式:
static const char *mainClass = "test/com/jstest/MainActivity"; // path of Java file #define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0]))) JNIEXPORT jstring JNICALL get_hello(JNIEnv *env, jclass clazz) { return (*env)->NewStringUTF(env, "hello from jni"); } static JNINativeMethod g_methods[] = { {"getHello", "()Ljava/lang/String;", (void *) get_hello}, }; JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = NULL; jint result = JNI_ERR; if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } assert(env != NULL); clazz = (*env)->FindClass(env, className); if (clazz == NULL) { return JNI_ERR; } // C 与 C++ 不同之处, if ((*env)->RegisterNatives(env, clazz, gMethods, NELEM(gMethods)) < 0) { return JNI_ERR; } // 代表 JNI 不同的版本. result = JNI_VERSION_1_6; return result; }
C++版本的动态注册如下所示:
static const char *mainClass = "test/com/jstest/MainActivity"; // path of Java file #define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0]))) JNIEXPORT jstring JNICALL get_hello(JNIEnv *env, jclass clazz) { return env->NewStringUTF("hello from jni"); } static JNINativeMethod g_methods[] = { {"getHello", "()Ljava/lang/String;", (void *) get_hello}, }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) { JNIEnv *env = NULL; jint result = JNI_FALSE; //获取env指针 if (jvm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { return result; } if (env == NULL) { return result; } //获取类引用,写类的全路径(包名+类名)。FindClass等JNI函数将在后面讲解 jclass clazz = env->FindClass(mainClass); if (clazz == NULL) { return result; } //注册方法 if (env->RegisterNatives(clazz, g_methods, NELEM(g_methods)) < 0) { return result; } //成功 result = JNI_VERSION_1_6; return result; }
垃圾回收
在 Java 代码中的对象是有垃圾回收机制的,但是对于 Native 中的代码应该如何进行垃圾 回收呢?以及如何同 Java 代码进行配合呢? 首先来看一段代码片段:
void Java_package_name_className(JNIEnv* env, jobject thiz) { ... //将由 Java 层传入的对象保存下来,已备后期使用. jboject save_thiz = thiz; ... return; }
查看上面的代码,我们能否在 native 方法的其他位置正常的使用 savethiz 代表的对象呢?结论是在其他 位置使用 savethiz 会出现问题,因为由于 Java 层有垃圾回收机制,当在 native 层使用 savethiz 对象时,有可能 Java 已经把 savethiz 指向的对象回收了,所以不能正常的使 用 savethiz.造成这个问题的核心原因是:thiz 对 savethiz的赋值操作*没有触发对象的 引用计数*.
如何处理上面的情况呢?
JNI 提供了三种类型的应用解决上面的问题:
- Local Reference: 本地引用,在 JNI 层的函数中使用的*非全局*引用对象都是 Local Reference, 它包含函数调用时传入的 jobject 和在 JNI 层中创建的 jobject.上面的 例子中的 thiz 和 savethiz 都是 Local Reference.它最大的特点是*一旦 JNI 层函 数返回,这些 jobject 就可能会被垃圾回收*.
- Global Reference: 全局引用,这种对象如果不主动的去释放,它永远不会被垃圾回收.
- Weak Global Reference: 弱全局引用,一种特殊的 Global Reference,在运行过程中有 可能被垃圾回收,所以在使用它之前要调用 JNIEnv 的 isSameObject 来判断它是否被回 收了.
我们看下面的例子:
MyMediaScannerClient(JNIEnv* env, jobject, client) : mEnv(env), // 调用 NewGlobalRef 创建一个 Global Reference mClient(env->NewGlobalRef(client)), mScanFileMethodID(0), ... { .... } // 析构函数 virtual ~MyMediaScannerClient(){ // 调用 DeleteGlobalRef 是否对这个对象的全局引用. mEvn->DeleteGlobalRef(mClient); }
参考链接
- Android NDK 开发之必知必会
- Android 官方 cmake 说明
- (卷一), 孟凡平