JNI 是 Java Native Interface 的缩写,它允许 Java 代码与本地代码(如 C/C++)相互调用。以下是 JNI 的全面使用指南。
1. JNI 基础概念
主要功能
-
从 Java 调用本地方法(C/C++ 实现)
-
从本地代码调用 Java 方法
-
在本地代码中操作 Java 对象
基本原理
-
Java 代码声明
native
方法 -
使用
javah
/javac -h
生成头文件 -
实现本地方法
-
编译生成动态链接库
-
Java 程序加载库并调用本地方法
2. 从 Java 调用本地代码
步骤 1:声明 native 方法
java
public class NativeDemo {
// 声明 native 方法
public native void sayHello();
public native int addNumbers(int a, int b);
// 加载动态库
static {
System.loadLibrary("NativeDemo");
}
public static void main(String[] args) {
NativeDemo demo = new NativeDemo();
demo.sayHello();
System.out.println("3 + 5 = " + demo.addNumbers(3, 5));
}
}
步骤 2:生成头文件
bash
javac NativeDemo.java
javac -h . NativeDemo.java
生成的头文件 NativeDemo.h
内容示例:
c
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class NativeDemo */
#ifndef _Included_NativeDemo
#define _Included_NativeDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: NativeDemo
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_NativeDemo_sayHello
(JNIEnv *, jobject);
/*
* Class: NativeDemo
* Method: addNumbers
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_NativeDemo_addNumbers
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
步骤 3:实现本地方法
创建 NativeDemo.c
文件:
c
#include <stdio.h>
#include "NativeDemo.h"
JNIEXPORT void JNICALL Java_NativeDemo_sayHello(JNIEnv *env, jobject obj) {
printf("Hello from native code!\n");
}
JNIEXPORT jint JNICALL Java_NativeDemo_addNumbers(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b;
}
步骤 4:编译本地代码
Linux/macOS:
bash
gcc -shared -fpic -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -o libNativeDemo.so NativeDemo.c
Windows:
cmd
cl -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -LD NativeDemo.c -FeNativeDemo.dll
步骤 5:运行 Java 程序
bash
java -Djava.library.path=. NativeDemo
3. 从本地代码调用 Java
访问 Java 对象字段
c
JNIEXPORT void JNICALL Java_NativeDemo_modifyField(JNIEnv *env, jobject obj) {
// 获取类
jclass cls = (*env)->GetObjectClass(env, obj);
// 获取字段ID
jfieldID fid = (*env)->GetFieldID(env, cls, "myField", "I");
// 获取字段值
jint value = (*env)->GetIntField(env, obj, fid);
// 修改字段值
(*env)->SetIntField(env, obj, fid, value + 1);
}
调用 Java 方法
c
JNIEXPORT void JNICALL Java_NativeDemo_callJavaMethod(JNIEnv *env, jobject obj) {
// 获取类
jclass cls = (*env)->GetObjectClass(env, obj);
// 获取方法ID
jmethodID mid = (*env)->GetMethodID(env, cls, "javaMethod", "(Ljava/lang/String;)V");
// 创建字符串参数
jstring jstr = (*env)->NewStringUTF(env, "From native code");
// 调用方法
(*env)->CallVoidMethod(env, obj, mid, jstr);
// 释放本地引用
(*env)->DeleteLocalRef(env, jstr);
}
4. 数据类型映射
基本类型
Java 类型 | JNI 类型 | C/C++ 类型 |
---|---|---|
boolean | jboolean | unsigned char |
byte | jbyte | char |
char | jchar | unsigned short |
short | jshort | short |
int | jint | int |
long | jlong | long long |
float | jfloat | float |
double | jdouble | double |
引用类型
Java 类型 | JNI 类型 |
---|---|
Object | jobject |
String | jstring |
Class | jclass |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
... | ... |
5. 异常处理
检查异常
c
jthrowable exc = (*env)->ExceptionOccurred(env);
if (exc) {
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
// 处理异常...
}
抛出异常
c
jclass excCls = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
if (excCls != NULL) {
(*env)->ThrowNew(env, excCls, "Invalid argument from native code");
}
6. 内存管理
局部引用
-
默认创建的引用都是局部引用
-
在本地方法返回后自动释放
-
可以手动释放:
DeleteLocalRef()
全局引用
c
jclass globalCls = (*env)->NewGlobalRef(env, localCls);
// 使用...
(*env)->DeleteGlobalRef(env, globalCls);
弱全局引用
c
jclass weakCls = (*env)->NewWeakGlobalRef(env, localCls);
// 使用前检查是否有效
if ((*env)->IsSameObject(env, weakCls, NULL) == JNI_FALSE) {
// 仍然有效
}
(*env)->DeleteWeakGlobalRef(env, weakCls);
7. 多线程注意事项
附加本地线程到JVM
c
JavaVM *jvm; // 需要保存全局的JavaVM指针
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
// 执行JNI调用...
(*jvm)->DetachCurrentThread(jvm);
线程同步
c
// 获取对象监视器
(*env)->MonitorEnter(env, obj);
// 临界区代码...
(*env)->MonitorExit(env, obj);
8. 最佳实践
-
减少JNI调用:每次JNI调用都有开销,尽量批量处理数据
-
正确处理字符串:及时释放GetStringUTFChars获取的字符串
-
异常检查:每次JNI调用后检查异常
-
资源释放:及时释放创建的全局引用和分配的内存
-
线程安全:确保多线程环境下正确使用JNI
-
类型签名:确保方法签名完全正确
9. 常见问题解决
库加载失败
-
确保库路径正确 (
java.library.path
) -
检查库依赖 (
ldd
/otool -L
) -
确保库文件名正确 (Linux:
libxxx.so
, Windows:xxx.dll
)
UnsatisfiedLinkError
-
检查方法名是否完全匹配 (包括包名)
-
检查方法签名是否匹配
-
确保库导出所需的符号
内存泄漏
-
检查全局引用是否释放
-
检查是否释放了GetXXXArrayElements获取的数组
-
检查是否释放了GetStringUTFChars获取的字符串
JNI 是连接 Java 和本地代码的强大工具,正确使用可以充分发挥两种语言的优势,但需要注意正确处理内存、异常和线程安全等问题。