likes
comments
collection
share

JNI入门简要指南

作者站长头像
站长
· 阅读数 15

什么是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… )。

  1. 在java中创建jni方法:
class NativeLib {
    public native void printMsg(String msg);
}
  1. 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
  1. 然后编写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);
}
  1. 使用编译工具将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
  1. 将生成的动态库移动到项目的lib目录下,在Java/Kt中加载动态库并调用jni方法。
// 加载lib
System.loadLibrary("nativelib")
// 调用方法
ClassName().printMsg("hello")
  1. 编译运行Java项目,运行程序。

JNI数据类型转换

JNI在应用开发中主要起连接Java层和native层的作用,两层之间数据类型是不同的,要使用另一层传入的数据或者为另一层提供数据,都需要通过JNI做转换处理。

基础类型

Java中的基础类型在jni中分别有对应的jxxx类型与其对应,比如 int 对应 jint ,而 jxxx 类型又是使用 typedef 为C类型定义的别名,因此它们之间可以直接互相转换。具体参见下表:

Java类型JNI类型C类型
booleanjbooleanuint8_t
bytejbyteint8_t
charjcharuint16_t
shortjshortint16_t
intjintint32_t
longjlongint64_t
floatjfloatfloat
doublejdoubledouble
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类型
Objectjobject
Classjclass
Stringjstring
Throwablejthrowable
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类型类型签名
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
voidV
类,如StringL+包名(/)+类名+;,如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对象:一是直接通过 JNIEnvNewObject 方法直接new出一个对象;二是先使用 JNIEnvAllocObject 分配对象的内存空间并初始化内存空间,然后通过 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.hpthread_create 创建子线程,但默认情况下, JNIEnv 无法在子线程中使用,如要在Native子线程中访问Java,则需要将创建的线程附着到Java虚拟机上。

pthread_t handle;
    // 1.创建线程
        // 第一个参数是线程句柄
        // 第二个参数用来指定新线程的一些属性,如栈大小、优先级等
        // 第三个参数为线程启动后要执行的函数的指针
        // 第四个参数为要传给执行函数的参数
pthread_create(&amp;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 **) &amp;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(&amp;env, nullptr) == 0) {
        // 5.使用JNIEnv
        ...
        // 6.执行结束后需要将当前线程从Jvm上移除
        gVm->DetachCurrentThread();
    }
    // 必须有返回值
    return nullptr;
}

JNI引用类型

JNI中并不能像在Java代码中那样方便的管理引用,为此JNI提供了三种引用类型用于管理在native中创建的Java对象:

  • 局部引用
  • 全局引用
  • 弱全局引用

局部引用

局部引用是最常见的一种引用,通过各种JNI接口(如 FindClassNewObjectGetObjectClas 等)以及 NewLocalRef 创建。局部引用无法直接赋值给静态变量或全局变量,因为它只在本地方法作用域中存在,当方法返回后,对象就会被JVM释放,因此无法跨方法/线程使用。

局部引用会被记录在每个native方法的局部引用表中,这个表是有大小限制的,如果创建太多的局部引用而不释放则可能会导致局部引用表溢出。

通常情况下我们可以依赖本地方法退出后局部引用的自动释放,但是在以下情况下,我们需要手动通过JNIEnv的 DeleteLocalRef 函数释放局部引用:

  1. 当我们需要在本地方法运行期间创建大量的局部引用对象时。
  2. 编写JNI工具函数时,如果native调用的其它JNI工具函数中会创建临时java对象,则最好在不用时及时释放它们。
  3. 当本地方法会长期执行时,比如里面写了一个死循环用来处理消息。
  4. 对于大对象的引用,应在不再使用时及时释放。因为局部引用存在时,会阻止对象被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 判断是否已经被回收,如果未被回收,则最好使用 NewLocalRefNewGlobalRef 将其提升为强引用,以避免在使用过程中被回收。

另外,尽管弱全局引用不会阻碍对象的释放,但是其引用本身也会占据一定的内存,并且即使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;
}

拓展阅读