4. JNI函数注册与调用
JNI 的调用关系不是“Java 调 C 函数”这么简单,它本质上是一套由 JVM 维护的符号绑定与调用约定体系。理解静态注册与动态注册的差异,掌握 JNI_OnLoad 的时机与职责,再配合方法查找、异常检查和调用约定,才能在生产环境中稳定实现 Java 与 C/C++ 的双向调用。
静态注册:名字即契约
静态注册依赖命名规则。Java 侧声明 native 方法后,C/C++ 侧必须提供形如 Java_包名_类名_方法名 的导出函数,包名中的点替换为下划线。例如:
// Java
package com.example.jni;
public class NativeBridge {
public static native int add(int a, int b);
}
// C++
extern "C" JNIEXPORT jint JNICALL
Java_com_example_jni_NativeBridge_add(JNIEnv* env, jclass clazz, jint a, jint b) {
return a + b;
}
静态注册的优点是直观、无需额外注册代码;缺点是函数名随包名和类名变化而变长,重构类名时容易漏改,且首次调用时 JVM 需要按名字查找符号,绑定发生在类加载后的首次调用阶段。若符号缺失,会在调用时报 UnsatisfiedLinkError,错误信息通常包含期望的函数名,这是排查静态注册问题的第一线索。
动态注册:在 JNI_OnLoad 中建立映射
动态注册通过 RegisterNatives 在运行时把 Java 方法与 C/C++ 函数指针绑定,函数名可以自由命名。它通常放在 JNI_OnLoad 中执行,因为该函数在 System.loadLibrary 加载库后、任何 native 方法被调用前由 JVM 回调,是注册的最佳时机。
static jint native_add(JNIEnv* env, jclass clazz, jint a, jint b) {
return a + b;
}
static const JNINativeMethod gMethods[] = {
{"add", "(II)I", (void*)native_add},
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = nullptr;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass clazz = env->FindClass("com/example/jni/NativeBridge");
if (clazz == nullptr) return JNI_ERR;
if (env->RegisterNatives(clazz, gMethods, 1) != JNI_OK) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
动态注册的关键参数是签名 (II)I,它描述参数与返回值类型。签名写错不会在编译期报错,而是在注册或调用时失败,因此必须与 Java 声明严格一致。常见类型签名:I 为 int,J 为 long,F 为 float,D 为 double,Z 为 boolean,Ljava/lang/String; 为 String,[I 为 int 数组。
方法查找:从 C/C++ 反向调用 Java
双向调用的另一半是 C/C++ 主动调用 Java。这需要先拿到目标类的 jclass,再获取方法或字段的 jmethodID/jfieldID,最后通过 CallXxxMethod 系列函数执行。查找类时使用全限定名,点替换为斜杠:
jclass cls = env->FindClass("com/example/jni/Callback");
jmethodID mid = env->GetStaticMethodID(cls, "onResult", "(ILjava/lang/String;)V");
jstring msg = env->NewStringUTF("done");
env->CallStaticVoidMethod(cls, mid, 0, msg);
env->DeleteLocalRef(msg);
env->DeleteLocalRef(cls);
方法查找有两个生产级注意点。第一,FindClass 在非 Java 线程中可能失败,因为此时没有正确的类加载器上下文;稳妥做法是在 JNI_OnLoad 中提前缓存 jclass 全局引用,或通过 GetObjectClass 从已有对象获取。第二,jmethodID 和 jfieldID 在类被卸载前有效,可以缓存复用,但 jclass 局部引用不能跨线程或跨调用长期持有,需要转为全局引用。
异常检查:JNI 中最容易被忽略的稳定性问题
JNI 函数不会像 C++ 异常那样自动中断执行。当 Java 侧抛出异常时,JVM 只是把异常挂起,C/C++ 代码仍会继续往下走。如果此时继续调用大多数 JNI 函数,行为未定义,常见结果是崩溃或二次异常。因此每次可能触发异常的 JNI 调用后,都应检查:
jclass cls = env->FindClass("com/example/jni/Callback");
if (env->ExceptionCheck()) {
env->ExceptionDescribe(); // 调试期打印堆栈
env->ExceptionClear(); // 清除挂起异常
return; // 不再继续调用 JNI
}
生产代码中应避免在异常挂起时调用 ExceptionDescribe 以外的 JNI 函数,并确保在返回 Java 层前决定是清除异常还是让它继续传播。若 native 方法本身要抛出 Java 异常,可用 ThrowNew:
jclass exCls = env->FindClass("java/lang/IllegalArgumentException");
env->ThrowNew(exCls, "invalid argument");
return 0;
调用约定与线程模型
JNI 调用约定包含几个层面:函数命名与导出约定、参数传递约定、以及线程附着约定。C++ 中必须用 extern "C" 避免名称修饰,否则静态注册找不到符号。参数上,JNIEnv* 是每个线程独立的,不能跨线程传递;jobject/jclass 是引用,不是指针,不能当普通 C 指针使用。
当 native 线程需要回调 Java 时,必须先通过 JavaVM 附着到 JVM:
JavaVM* gVm; // 在 JNI_OnLoad 中保存
JNIEnv* env = nullptr;
bool attached = false;
if (gVm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
if (gVm->AttachCurrentThread(&env, nullptr) != JNI_OK) return;
attached = true;
}
// ... 调用 Java ...
if (attached) gVm->DetachCurrentThread();
线程附着与分离必须成对出现,否则线程退出时 JVM 可能无法正常回收资源。对于频繁回调的场景,可以保持附着状态,但要在明确的线程生命周期结束时分离。
GetStaticMethodID 反查一次,确认签名与 Java 声明一致,把问题提前到加载阶段。CallXxxMethod、NewStringUTF 等函数属于未定义行为。排查崩溃时,优先检查每个 JNI 调用后是否遗漏了 ExceptionCheck。把静态注册、动态注册、JNI_OnLoad、方法查找、异常检查和调用约定串起来看,核心思路是:注册阶段确定绑定关系,调用阶段保证线程与引用合法,异常阶段及时检查并清理。三者都做到,Java 与 C/C++ 的双向调用才能在生产环境中保持稳定。