3. JNI基础与类型系统

JNI 的稳定性问题,绝大多数最终都能追溯到类型系统理解不到位:把 jobject 当成 C++ 对象直接解引用、把局部引用当全局引用长期持有、签名写错导致 NoSuchMethodError、把 jstring 当 char* 使用。本节把 JNI 的类型系统拆成三层来看:运行环境层(JNIEnv)、对象句柄层(jobject/jclass/jmethodID/jfieldID)、类型映射层(基本类型与引用类型、签名规则)。三层理解清楚,后面章节的引用管理、异常处理、线程绑定才有落脚点。

一、JNIEnv:线程私有的函数表指针

在 C 语言中,JNIEnv 是 const struct JNINativeInterface* 的别名,本质是一个指向函数表的指针;在 C++ 中,它是 _JNIEnv 结构体,内部同样持有一个函数表指针,但把函数封装成成员方法,因此调用时不需要显式传 env。两种写法等价:

// C 风格
JNIEXPORT jstring JNICALL Java_com_example_Native_greet(JNIEnv* env, jobject thiz) {
    return (*env)->NewStringUTF(env, "hello");
}

// C++ 风格
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_Native_greet(JNIEnv* env, jobject thiz) {
    return env->NewStringUTF("hello");
}

关键约束是:JNIEnv 与线程绑定,不能跨线程传递。同一个线程内多次调用 JNI 方法,JNIEnv 指针通常相同;但把某个线程的 JNIEnv 缓存到全局变量,再在另一个线程使用,属于未定义行为。子线程要调用 JNI,必须通过 JavaVM::AttachCurrentThread 获取属于该线程的 JNIEnv,退出前 DetachCurrentThread。这是后续“线程与 JNIEnv 绑定”章节的核心前提。

二、对象句柄:jobject、jclass、jmethodID、jfieldID

JNI 不暴露 Java 对象的内存布局,所有 Java 侧实体在 native 侧都以不透明句柄表示:

  • jobject:任意 Java 对象的通用句柄,包括实例、数组、字符串。
  • jclass:类对象的句柄,可由 FindClass、GetObjectClass 或 GetSuperclass 获得。
  • jmethodID:方法标识,由 GetMethodID / GetStaticMethodID 返回,用于 CallXxxMethod。
  • jfieldID:字段标识,由 GetFieldID / GetStaticFieldID 返回,用于 GetXxxField / SetXxxField。

这四者的生命周期差异极大,是生产事故高发区。jobject 和 jclass 属于引用,受局部/全局/弱全局引用规则约束,局部引用在 native 方法返回后自动失效;而 jmethodID 和 jfieldID 是标识符,只要对应类未被卸载就一直有效,可以安全地缓存到全局变量中复用。很多团队把 jclass 缓存成全局引用却忘了释放,或者把 jmethodID 当成需要 Delete 的引用,都是把两类语义混为一谈。

判断口诀:需要 NewGlobalRef/DeleteGlobalRef 管理的是 jobject/jclass;jmethodID/jfieldID 只需缓存,不需要释放。

三、基本类型映射与引用类型转换

Java 的八种基本类型在 JNI 中有固定的一一映射,宽度明确、无平台歧义:

Java 类型JNI 类型宽度签名
booleanjboolean8 位无符号Z
bytejbyte8 位有符号B
charjchar16 位无符号C
shortjshort16 位有符号S
intjint32 位有符号I
longjlong64 位有符号J
floatjfloat32 位F
doublejdouble64 位D

注意 long 对应 jlong(64 位),而 C 的 long 在 32 位平台只有 32 位,直接强转会在 32 位 ABI 上截断。引用类型则没有直接映射,必须通过 JNI 函数转换:jstring 用 GetStringUTFChars/ReleaseStringUTFChars 或 GetStringChars 转换;数组用 GetIntArrayElements 等;普通对象只能通过方法调用或字段访问逐项读取,不能整体 memcpy。

// jstring 转换:必须成对释放,且注意 UTF-8 修改版编码
const char* utf = env->GetStringUTFChars(jstr, nullptr);
if (utf == nullptr) return; // OOM,异常已挂起
size_t len = strlen(utf);
// ... 使用 utf ...
env->ReleaseStringUTFChars(jstr, utf);

GetStringUTFChars 返回的是 Modified UTF-8,与标准 UTF-8 在补充平面字符(如 emoji)和 \0 上不同:Java 的 String 允许内嵌 \0,Modified UTF-8 会把它编码为两字节 0xC0 0x80,而补充平面字符以 CESU-8 代理对形式存储。若 native 侧要交给标准 UTF-8 解析器,需要额外转换,否则会出现乱码或截断。

四、签名规则:JNI 类型描述符

方法签名由参数类型描述符按顺序拼接,加返回类型描述符构成,格式为 (参数...)返回类型。引用类型用 L包名/类名; 表示,包名中的点替换为斜杠,末尾必须带分号;数组用 [ 前缀,如 [I 表示 int[],[[Ljava/lang/String; 表示 String[][]。

Java 声明JNI 签名
void foo()()V
int add(int a, int b)(II)I
String name()()Ljava/lang/String;
void set(int[] arr, long id)([IJ)V
Object get(String k, boolean d)(Ljava/lang/String;Z)Ljava/lang/Object;

签名写错是 NoSuchMethodError 的直接原因,且错误信息往往只报方法名不报签名,排查成本高。生产上推荐用 javap -s -p 直接导出签名,而不是手写:

javap -s -p com.example.NativeBridge
// 输出示例:
// public native void onEvent(int, java.lang.String);
//   descriptor: (ILjava/lang/String;)V

下面这张图把 JNI 类型系统的三层关系与生命周期差异集中呈现,便于对照记忆:

运行环境层 · 线程私有 JNIEnv* 函数表指针 / 线程绑定 JavaVM* 进程级 / 可跨线程 AttachCurrentThread → 获取本线程 JNIEnv 禁止跨线程传递 JNIEnv 对象句柄层 · 引用 vs 标识符 jobject / jclass 引用类型 需 New/DeleteGlobalRef 局部引用方法返回即失效 jmethodID 标识符 可全局缓存复用 无需释放 jfieldID 标识符 可全局缓存复用 无需释放 常见错误 缓存局部引用 Delete jmethodID 跨线程用 JNIEnv 类型映射层 · 基本类型与引用类型 基本类型 · 直接映射 jint / jlong / jfloat / jdouble jboolean / jbyte / jchar / jshort 注意 jlong 与 C long 宽度差异 引用类型 · 函数转换 jstring → GetStringUTFChars jarray → GetXxxArrayElements Modified UTF-8 ≠ 标准 UTF-8 签名规则 · 描述符 (参数...)返回类型 L包/类; [数组前缀 javap -s -p 导出签名

五、生产实践中的类型系统检查清单

把上述规则落到代码审查和排障中,可以形成一份可执行的检查清单:

  1. 所有 GetStringUTFChars / GetXxxArrayElements 是否与对应的 Release 成对出现,且异常路径也覆盖。
  2. 缓存到全局的 jclass 是否用 NewGlobalRef 创建,并在卸载时 DeleteGlobalRef。
  3. jmethodID / jfieldID 是否被误当作引用释放。
  4. 子线程是否通过 AttachCurrentThread 获取 JNIEnv,而非复用主线程的指针。
  5. 方法签名是否由 javap -s -p 导出,而非手写。
  6. 跨语言传递的字符串是否确认过 Modified UTF-8 与标准 UTF-8 的差异。

理解这三层之后,后续的局部/全局引用管理、异常检查、线程绑定与性能优化,都可以看作是在这套类型系统上做“生命周期”和“边界”的加固。类型系统是 JNI 稳定性的地基,地基不牢,后面的优化手段都会变成补丁。