JNI入门简要指南
什么是JNI?
Java本地开发接口,英文全称是 Java Native Interface。 JNI是一个协议。这个协议用来沟通Java代码和外部的本地代码(C/C++),通过这个协议 ,Java代码可以调用外部的C/C++代码 ,外部的C/C++代码也可以调用本地的Java代码。
从Java调用Native函数开始
一般来说,JNI的开发步骤大致有以下5步(对于Android开发,我们可以通过AndroidStudio快速体验JNI开发,参考 www.jianshu.com/p/be6e931a1… )。
- 在java中创建jni方法:
class NativeLib {
public native void printMsg(String msg);
}
- 在jni方法所在目录下使用
javac
生成对应的native方法签名。
$ javac -encoding utf8 -h NativeLib.java
执行上述命令将得到一个头文件,其中包含了对应的native函数声明。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_package_NativeLib */
#ifndef _Included_com_example_package_NativeLib
#define _Included_com_example_package_NativeLib
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_package_NativeLib
* Method: printMsg
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_example_package_NativeLib_printMsg
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
- 然后编写native函数的定义。
extern "C"
JNIEXPORT void JNICALL
Java_com_example_package_NativeLib_printMsg(JNIEnv *env, jobject thiz, jstring msg) {
const char *cmsg = env->GetStringUTFChars(msg, 0);
printf("%s", cmsg);
}
- 使用编译工具将native代码编译成动态库。具体编译方式取决于你使用的工具链以及目标平台。此处以Linux平台上使用GCC为例子:
gcc nativelib.cpp -std=c++11 -fPIC -shared -I/usr/local/java/jdk1.8.0_144/include -I/usr/local/java/jdk1.8.0_144/include/linux -lm -o ./nativelib.so
- 将生成的动态库移动到项目的lib目录下,在Java/Kt中加载动态库并调用jni方法。
// 加载lib
System.loadLibrary("nativelib")
// 调用方法
ClassName().printMsg("hello")
- 编译运行Java项目,运行程序。
JNI数据类型转换
JNI在应用开发中主要起连接Java层和native层的作用,两层之间数据类型是不同的,要使用另一层传入的数据或者为另一层提供数据,都需要通过JNI做转换处理。
基础类型
Java中的基础类型在jni中分别有对应的jxxx类型与其对应,比如 int
对应 jint
,而 jxxx
类型又是使用 typedef
为C类型定义的别名,因此它们之间可以直接互相转换。具体参见下表:
Java类型 | JNI类型 | C类型 |
---|---|---|
boolean | jboolean | uint8_t |
byte | jbyte | int8_t |
char | jchar | uint16_t |
short | jshort | int16_t |
int | jint | int32_t |
long | jlong | int64_t |
float | jfloat | float |
double | jdouble | double |
void | - | void |
字符串
jni传递的字符串是java中的对象,需要做特殊的处理才能转换为C风格的字符串。
extern "C"
JNIEXPORT void JNICALL
Java_xyz_dean_demo_nativelib_NativeLib_printMsg(
JNIEnv *env, jobject thiz, jstring msg
) {
// Java string 转 C string
const char *cstr = env->GetStringUTFChars(msg, false);
if (cstr == NULL) {
// GetStringUTFChars涉及到内存分配,可能会失败,需要做
// 好异常处理。
}
...
// c风格的string使用完后需要手动释放内存
env->ReleaseStringUTFChars(msg, cstr);
// C风格的字符串转jstring
jstring jstr = env->NewStringUTF("C Style String");
// jni中获取jstring字符串长度
jsize len = env->GetStringLength(msg);
// 截取字符串
jchar subStr[len];
env->GetStringRegion(jstr, 0, len - 1, subStr);
}
其它引用类型
除了基本类型之外,Java中的引用类型在C中并没有相对应的类型,所有对java对象的操作,都需要通过jni发送到jvm中去执行。下表是Java中类型与jni类型的对应关系:
Java类型 | JNI类型 |
---|---|
Object | jobject |
Class | jclass |
String | jstring |
Throwable | jthrowable |
Object[] | jobjectArray |
基本类型[] | j+基本类型+Array |
JNI访问Java对象
有如下Java类型:
public class User {
public static int count = 0;
public String name;
public int age;
@Override
public String toString() {
return "User{" +
"name='" + name + '\\'' +
", age=" + age +
'}';
}
public static int updateCount(int add) {
return count += add;
}
}
在JNI中可以通过JNIEnv问此对象:
// 获取java对象的class
jclass clazz = env->GetObjectClass(user);
// 获取类的普通字段id或者静态字段
// 第一个参数为对象的类型
// 第二个参数为字段名称
// 第三个参数为字段的类型签名(类型签名参见后表)
jfieldID ageFieldId = env->GetFieldID(clazz, "age", "I");
jfieldID nameFieldId = env->GetFieldID(clazz, "name", "Ljava/lang/String;");
jfieldID countSFieldId = env->GetStaticFieldID(clazz, "count", "I");
// 通过字段id访问对象的字段值
jint age = env->GetIntField(user, ageFieldId);
jobject nameObj = env->GetObjectField(user, nameFieldId);
if (nameObj == nullptr) return;
auto name = (jstring) nameObj;
jint count = env->GetStaticIntField(clazz, countSFieldId);
// 通过字段id为对象赋值
env->SetIntField(user, ageFieldId, age + 1);
jstring newName = env->NewStringUTF("NewName");
env->SetObjectField(user, nameFieldId, newName);
env->SetStaticIntField(clazz, countSFieldId, count + 1);
// 获取方法Id
// 第一个参数为对象类型
// 第二个参数为方法名称
// 第三个参数为方法签名(具体规则见后文)
jmethodID toStrId = env->GetMethodID(clazz, "toString", "()Ljava/lang/String;");
jmethodID upCountId = env->GetStaticMethodID(clazz, "updateCount", "(I)I");
// 通过方法id调用Java方法
// 根据方法类型和签名不同,有不同的调用方式,具体查看JNIEnv中的方法。
env->CallObjectMethod(user, toStrId);
env->CallStaticIntMethod(clazz, upCountId, 10);
类型签名
Java类型 | 类型签名 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
类,如String | L+包名(/)+类名+;,如Ljava/lang/String; |
数组,如String[] | [+元素的类型签名,如[java/lang/String; |
方法签名
方法签名由参数类型签名和返回类型的签名组成,具体规则如下:
(<参数1的类型签名><参数2的类型签名>...)<返回类型的签名>
// 注:参数之间没有分隔符号
// 如,对于java方法
public String getString(int a, int b, String c) { ... }
// 其方法签名为:
(IILjava/lang/String;)Ljava/lang/String;
JNI创建Java对象
有时候我们需要在Native中创建一个Java对象并返回给上层的Java,在JNI中,有两种方式创建Java对象:一是直接通过 JNIEnv
的 NewObject
方法直接new出一个对象;二是先使用 JNIEnv
的 AllocObject
分配对象的内存空间并初始化内存空间,然后通过 JNIEnv
调用该对象的构造函数来初始化此对象。
💡 JVM中对象创建主要有两大步骤,一是虚拟机执行new指令为对象分配内存空间,并且将这块空间都初始化为0值;然后是虚拟机执行该对象的构造方法,按程序员的意愿初始化对象。可以看到,JNI创建Java对象的第二种方式就很好的体现了这个过程。
有如下Java类型:
public class User {
public String name;
public User(String name) {
this.name = name;
}
}
使用JNI构造对象的方法如下:
// 查找要构建对象的类型
jclass clazz = env->FindClass("xyz/dean/demo/nativelib/User");
// 获取构造方法ID,构造方法名称为<init>,并且没有返回值。
// 因为构造方法实际是对象的初始化方法~
jmethodID consMethodId = env->GetMethodID(clazz, "<init>", "(Ljava/lang/String;)V");
// 创建构造函数所需的参数
jstring name = env->NewStringUTF("Name Zhangsan");
// 直接new出新的对象
jobject user1 = env->NewObject(clazz, consMethodId, name);
// 分配对象内存空间
jobject user2 = env->AllocObject(clazz);
// 执行构造函数初始化对象
env->CallVoidMethod(user2, consMethodId, name);
JNI子线程中访问Java
在Native中可以使用 pthread.h
的 pthread_create
创建子线程,但默认情况下, JNIEnv
无法在子线程中使用,如要在Native子线程中访问Java,则需要将创建的线程附着到Java虚拟机上。
pthread_t handle;
// 1.创建线程
// 第一个参数是线程句柄
// 第二个参数用来指定新线程的一些属性,如栈大小、优先级等
// 第三个参数为线程启动后要执行的函数的指针
// 第四个参数为要传给执行函数的参数
pthread_create(&handle, nullptr, threadAction, nullptr);
// 2.在JNI初始化方法中获取Java虚拟机对象并保存起来
static JavaVM *gVm = nullptr;
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
gVm = vm;
return JNI_VERSION_1_6;
}
// 3.定义线程执行函数
void *threadAction(void *) {
// 4.将当前线程附加到Java虚拟机上
JNIEnv *env;
if (gVm->AttachCurrentThread(&env, nullptr) == 0) {
// 5.使用JNIEnv
...
// 6.执行结束后需要将当前线程从Jvm上移除
gVm->DetachCurrentThread();
}
// 必须有返回值
return nullptr;
}
JNI引用类型
JNI中并不能像在Java代码中那样方便的管理引用,为此JNI提供了三种引用类型用于管理在native中创建的Java对象:
- 局部引用
- 全局引用
- 弱全局引用
局部引用
局部引用是最常见的一种引用,通过各种JNI接口(如 FindClass
、 NewObject
、 GetObjectClas
等)以及 NewLocalRef
创建。局部引用无法直接赋值给静态变量或全局变量,因为它只在本地方法作用域中存在,当方法返回后,对象就会被JVM释放,因此无法跨方法/线程使用。
局部引用会被记录在每个native方法的局部引用表中,这个表是有大小限制的,如果创建太多的局部引用而不释放则可能会导致局部引用表溢出。
通常情况下我们可以依赖本地方法退出后局部引用的自动释放,但是在以下情况下,我们需要手动通过JNIEnv的 DeleteLocalRef
函数释放局部引用:
- 当我们需要在本地方法运行期间创建大量的局部引用对象时。
- 编写JNI工具函数时,如果native调用的其它JNI工具函数中会创建临时java对象,则最好在不用时及时释放它们。
- 当本地方法会长期执行时,比如里面写了一个死循环用来处理消息。
- 对于大对象的引用,应在不再使用时及时释放。因为局部引用存在时,会阻止对象被GC回收,如果大对象不再使用又一直得不到释放,就会造成资源浪费。
除了主动调用 DeleteLocalRef
释放局部引用,JNI还提供了以下方式管理局部引用:
EnsureLocalCapacity
前面提到,局部引用会被记录在native方法的局部引用表中,这个表有大小限制,当创建局部引用的数目超过表的限制时,就会出错。使用 EnsureLocalCapacity
可以帮助我们判断当前线程中是否可以创建指定数量的局部引用,返回0则表示可以,非0表示失败。
int len = 10;
if (env->EnsureLocalCapacity(len) == 0) {
for (int i = 0; i < len; ++i) {
jstring jstr = env->GetObjectArrayElement(arr,i);
// 至少可以创建10个引用
}
} else {
// 容量不足
}
PushLocalFrame 与 PopLocalFrame
PushLocalFrame
可以创建一个容纳指定数量局部引用的内嵌空间,在这个空间创建的局部引用会被记录下来,当调用 PopLocalFrame
离开空间时则会自动释放这些局部引用。
// 创建指定数据的局部引用空间
if (env->PushLocalFrame(len) == 0) {
jstring jstr = env->GetObjectArrayElement(arr, i);
...
// 调用 PopLocalFrame 直接释放这个空间内的所有局部引用
env->PopLocalFrame(NULL);
} else {
// 容量不足
}
全局引用
在JNI中如果需要跨native方法或者线程使用Java对象,则需要将局部引用转为全局引用。JNIEnv提供了 NewGlobalRef
来将局部引用提升为全局引用,需要注意的是:全局引用也会阻止它所引用的对象被JVM回收,并且全局引用不会自动释放,需要手动调用 DeleteGlobalRef
释放。因此使用全局引用时需要非常注意所引用对象的生命周期,谨防泄漏。
static jclass strClass = nullptr;
extern "C"
JNIEXPORT void JNICALL
Java_xyz_dean_demo_nativelib_NativeLib_testGlobalRef(JNIEnv *env, jobject thiz) {
if (strClass == nullptr) {
jclass clz = env->FindClass("java/lang/String");
strClass = (jclass) env->NewGlobalRef(clz);
env->DeleteLocalRef(clz);
}
if (strClass != nullptr) {
jmethodID ini = env->GetMethodID(strClass, "<init>", "(Ljava/lang/String;)V");
jstring inStr = env->NewStringUTF("String");
jstring ret = (jstring) env->NewObject(strClass, ini, inStr);
env->DeleteLocalRef(inStr);
} else {
LOGI("Get class failed.");
return;
}
}
弱全局引用
弱全局引用与全局引用一样,可以跨native方法和线程使用,但是它不会阻止所引用对象被回收(即不会作为GC Root),当所引用的对象在其它位置没有强引用时,它就可能会被GC回收。
JNIEnv中提供了 NewWeakGlobalRef
来为其它引用创建一个弱全局引用,但需要注意的是,因为弱全局引用随时可能会被GC回收,因此在使用前需要使用 IsSameObject
判断是否已经被回收,如果未被回收,则最好使用 NewLocalRef
或 NewGlobalRef
将其提升为强引用,以避免在使用过程中被回收。
另外,尽管弱全局引用不会阻碍对象的释放,但是其引用本身也会占据一定的内存,并且即使JVM回收了其引用的对象,引用本身所占据的内存却不会被回收,这将造成缓慢的内存泄漏(slow memory leak),因此,当弱全局引用不再使用时,也需要调用 DeleteWeakGlobalRef
来释放它。
static jobject weakCache = nullptr;
extern "C"
JNIEXPORT void JNICALL
Java_xyz_dean_demo_nativelib_NativeLib_testWeakGlobalRef(JNIEnv *env, jobject thiz, jstring str) {
if (weakCache == nullptr) {
weakCache = env->NewWeakGlobalRef(str);
}
if (!env->IsSameObject(weakCache, nullptr)) {
jstring mtr = (jstring) env->NewLocalRef(weakCache);
...
env->DeleteLocalRef(mtr);
} else {
LOGI("Cached object has been GC.");
}
}
JNI中异常处理
在Java中我们可以使用 try-catch
捕获并处理异常,然而,受限于native函数可能由C这类没有异常处理机制的语言编写,故JNI中并不能像Java那般方便的处理异常。
JNI检查调用Java方法产生的异常
在JNI中,如果调用的Java方法会抛出异常,JNI函数并不会直接终止,而是会继续执行后面的代码。我们可以动通过JNIEnv的 ExceptionCheck
/ ExceptionOccurred
函数检查调用是否发生了异常,两者的差别在于前者返回布尔值,后者返回具体的异常引用。另外,当检查到异常后,JNIEnv还提供了 ExceptionDescribe
用于打印当前异常的堆栈信息, ExceptionClear
用于清除异常信息的缓冲区。通过这些函数,我们就能完成JNI调用中的异常检查与恢复。
jclass clazz = env->FindClass("xyz/dean/demo/nativelib/NativeLib");
jmethodID jmethod = env->GetStaticMethodID(clazz, "funcThrow", "()V");
env->CallStaticVoidMethod(clazz, jmethod);
if (env->ExceptionCheck()) {
// if (env->ExceptionOccurred() != nullptr) {
// ExceptionOccurred会创建局部引用,要记得释放~
env->ExceptionDescribe();
env->ExceptionClear();
// 异常恢复完毕,继续执行后续代码
// return;
}
LOGI("resume exec.");
有时候我们可能需要从native层向Java层返回异常,使用 Throw
可以将一个 jthrowable
异常引用抛出到JVM,或使用 ThrowNew
函数可以构造一个异常并抛出到JVM,需要注意的是 ThrowNew
也不会终止后续代码的运行,我们需要手动处理。
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
jclass thrclz = env->FindClass("java/lang/RuntimeException");
env->ThrowNew(clazz, "Runtime exception from native.");
env->DeleteLocalRef(thrclz);
return;
}
拓展阅读
- 一份还算详细的中文JNI博客 JNI/NDK_xyang0917的博客-CSDN博客
- 《Android C++高级编程 使用NDK》
- Oracle官方的JNI指引 Contents
转载自:https://juejin.cn/post/7133601846832136206