1、JNI静态与动态注册
在Java Native Interface (JNI) 中,注册指的是将Java方法与其本地实现进行关联。JNI提供了两种主要的注册方式:静态注册和动态注册。这两种方式各有优缺点和使用场景。下面对这两种注册方式进行详细说明。
1. 静态注册 (Static Registration)
定义
静态注册是指通过JNI生成的头文件中的函数名称来实现Java方法与本地实现的绑定。函数名称遵循特定的命名规则,以确保Java虚拟机能够正确地找到对应的本地实现。
特点
- 命名约定:本地函数的名称必须遵循特定的命名规则:
Java_类全限定名_方法名
。 - 代码自动生成:使用
javac -h
或javah
工具时,会自动生成带有正确函数名称的头文件。 - 简单易用:对于大多数应用场景来说,静态注册足够且容易实现。
实现步骤
- 编写Java代码,声明本地方法。
- 使用
javac -h
或javah
生成头文件。头文件中包含了本地方法的函数声明。 - 在本地代码中实现头文件中声明的函数。
示例
Java类:
public class Example {
public native void sayHello();
static {
System.loadLibrary("example");
}
}
生成头文件:
javac Example.java
javac -h . Example.java
头文件 (Example.h):
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Example */
#ifndef _Included_Example
#define _Included_Example
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Example
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Example_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
本地实现 (Example.c):
#include <jni.h>
#include "Example.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_Example_sayHello(JNIEnv *env, jobject obj) {
printf("Hello from native code!\n");
}
在此示例中,函数 Java_Example_sayHello
的名称是按照静态注册的命名规则定义的。Java虚拟机通过这种命名约定将Java代码中的本地方法与C/C++实现联系起来。
2. 动态注册 (Dynamic Registration)
定义
动态注册允许开发者在本地库加载时,以编程的方式将Java方法与本地实现关联。与静态注册不同,动态注册不要求本地函数名遵循特定命名规则。
特点
- 灵活性:不依赖于固定的命名规则,开发者可以自行决定函数名称。
- 集中管理:所有的注册都可以集中在一个地方进行,便于管理。
- 复杂性:实现比静态注册复杂,需要编写额外的注册代码。
实现步骤
- 编写Java代码,声明本地方法。
- 实现JNI_OnLoad函数,并在其中使用
JNIEnv
的RegisterNatives
方法注册本地方法。 - 在本地代码中定义本地方法实现。
示例
Java类:
public class Example {
public native void sayHello();
static {
System.loadLibrary("example");
}
}
本地实现 (Example.c):
#include <jni.h>
#include <stdio.h>
// 本地方法的实现
void nativeSayHello(JNIEnv *env, jobject obj) {
printf("Hello from native code (dynamic registration)!\n");
}
// 方法表,包含Java方法名、签名以及对应的本地函数指针
static JNINativeMethod method_table[] = {
{"sayHello", "()V", (void *) nativeSayHello}
};
// 在库加载时注册本地方法
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR; // 加载失败
}
jclass cls = (*env)->FindClass(env, "Example");
if (cls == NULL) {
return JNI_ERR;
}
// 注册本地方法
if ((*env)->RegisterNatives(env, cls, method_table, sizeof(method_table) / sizeof(method_table[0])) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
在这个例子中,nativeSayHello
是本地方法的实现,它并没有遵循静态注册的命名规则。相反,我们在 JNI_OnLoad
函数中,使用 RegisterNatives
函数将 sayHello
方法与 nativeSayHello
进行关联。
3. 动态与静态注册的比较
-
静态注册:
- 优点:简单易用,不需要额外的注册代码。
- 缺点:受限于命名规则,不够灵活。
-
动态注册:
- 优点:灵活性更高,不依赖于特定的命名规则。可以集中管理注册过程,方便维护。
- 缺点:需要额外的代码实现注册,复杂度较高。
4. 选择使用场景
- 静态注册:适合简单的JNI应用,方法较少且无需动态特性时。
- 动态注册:适合复杂应用场景,特别是需要动态加载类或者方法名不固定时。
动态注册可以提供更大的灵活性,尤其是在需要动态加载或运行时确定的方法时。然而,静态注册通常足以满足大多数常见的应用场景,并且实现起来更简单。开发者可以根据具体需求选择合适的注册方式。
2、方法签名、与Java通信
在JNI中,方法签名和与Java通信是重要的概念。方法签名用于识别Java方法的参数和返回类型,而与Java通信涉及到在Java和本地代码之间传递数据和调用方法。下面将详细说明这些内容。
方法签名
方法签名是在JNI中用于唯一标识一个方法的字符串表示形式。它包括方法参数类型和返回类型的信息。方法签名对于JNI至关重要,因为它帮助JNI确定如何在本地代码和Java代码之间进行正确的数据转换。
1. 基本规则
方法签名的格式如下:
(参数类型签名)返回类型签名
- 参数类型签名:由方法的所有参数类型的签名组成。
- 返回类型签名:由返回类型的签名组成。
2. 常用类型签名
-
基本类型:
Z
:boolean
B
:byte
C
:char
S
:short
I
:int
J
:long
F
:float
D
:double
V
:void
(仅用于返回类型)
-
引用类型:
L类全限定名;
:类的类型。例如,Ljava/lang/String;
表示java.lang.String
。[类型签名
:数组类型。例如,[I
表示int[]
。
3. 示例
void method(int a, String b)
的签名为(ILjava/lang/String;)V
String[] method(byte[] data)
的签名为([B)[Ljava/lang/String;
与Java通信
与Java通信涉及在本地代码中调用Java方法、访问Java字段,以及处理Java对象和数据结构。以下是几种主要的通信方式:
1. 调用Java方法
本地代码可以调用Java对象的方法。为此,你需要:
1. 获取Java类:通过JNIEnv的FindClass方法获取Java类的jclass对象。
2. 获取方法ID:使用GetMethodID方法获取要调用的方法的ID。
3. 调用方法:使用Call<Type>Method系列函数调用Java方法。
示例:
假设我们有一个Java类Example
,它有一个方法public void callMe(String message)
。
public class Example {
public void callMe(String message) {
System.out.println("Message from JNI: " + message);
}
}
本地代码:
JNIEnv *env;
jobject obj; // Java对象
// 获取Example类
jclass cls = (*env)->GetObjectClass(env, obj);
// 获取callMe方法ID
jmethodID mid = (*env)->GetMethodID(env, cls, "callMe", "(Ljava/lang/String;)V");
if (mid == NULL) {
return; // 方法未找到
}
// 创建Java字符串
jstring jstr = (*env)->NewStringUTF(env, "Hello from native code!");
// 调用Java方法
(*env)->CallVoidMethod(env, obj, mid, jstr);
2. 访问Java字段
本地代码可以读取和修改Java对象的字段值。
步骤
1. 获取Java类:通过FindClass方法获取Java类的jclass对象。
2. 获取字段ID:使用GetFieldID(实例字段)或GetStaticFieldID(静态字段)方法获取字段的ID。
3. 访问字段值:使用Get<Type>Field(读取字段)和Set<Type>Field(设置字段)系列方法操作字段。
示例:
public class Example {
public int value;
}
本地代码:
jfieldID fid = (*env)->GetFieldID(env, cls, "value", "I");
if (fid == NULL) {
return; // 字段未找到
}
// 获取字段值
jint value = (*env)->GetIntField(env, obj, fid);
// 设置字段值
(*env)->SetIntField(env, obj, fid, value + 1);
3. 处理Java数组
JNI提供了一系列函数用于操作Java数组,例如创建数组、获取和设置数组元素等。
步骤
1. 获取数组类型:使用New<Type>Array方法创建数组。
2. 访问和修改元素:使用Get<Type>ArrayElements和Set<Type>ArrayElements方法操作数组元素。
3. 释放数组:使用Release<Type>ArrayElements释放数组元素。
示例:
public class Example {
public int[] numbers;
}
本地代码:
// 获取字段ID
jfieldID fid = (*env)->GetFieldID(env, cls, "numbers", "[I");
// 获取数组对象
jintArray arr = (*env)->GetObjectField(env, obj, fid);
// 获取数组长度
jsize length = (*env)->GetArrayLength(env, arr);
// 获取数组元素
jint *elements = (*env)->GetIntArrayElements(env, arr, NULL);
// 处理数组元素
for (int i = 0; i < length; i++) {
elements[i] += 1; // 每个元素加1
}
// 释放数组
(*env)->ReleaseIntArrayElements(env, arr, elements, 0);
4. 异常处理
本地代码可以检查是否有Java异常发生,并可以从本地代码抛出Java异常。
检查异常:
if ((*env)->ExceptionCheck(env)) {
// 处理异常
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
}
抛出异常:
jclass exceptionCls = (*env)->FindClass(env, "java/lang/Exception");
if (exceptionCls != NULL) {
(*env)->ThrowNew(env, exceptionCls, "Error message from native code.");
}
5. 处理Java字符串
本地代码中常常需要处理Java字符串。可以使用GetStringUTFChars
将Java字符串转换为C字符串,并在处理后释放资源。
示例:
jstring javaString = ...;
const char *cString = (*env)->GetStringUTFChars(env, javaString, NULL);
// 使用cString
(*env)->ReleaseStringUTFChars(env, javaString, cString);
总结
在JNI中,方法签名是用于描述Java方法参数和返回类型的字符串,与Java通信则包括调用Java方法、访问Java字段、处理Java数组、异常处理和字符串操作等方面。这些机制使得本地代码可以与Java代码进行有效的交互,实现复杂的功能。
3、本地引用与全局引用
在JNI(Java Native Interface)中,本地引用(Local References)和全局引用(Global References)是两种管理Java对象的引用类型。它们的主要区别在于生命周期和使用场景。理解它们的区别对于有效管理内存和避免内存泄漏至关重要。
1. 本地引用 (Local References)
定义
本地引用是在JNI中本地方法调用期间创建的Java对象的引用。这些引用在本地方法返回后自动失效,由JVM自动管理和释放。
特点
- 生命周期短:本地引用仅在本地方法的执行期间有效,一旦方法返回,引用即被释放。
- 自动管理:JVM自动管理本地引用的生命周期,因此不需要显式释放。
- 数量有限:JVM对本地引用的数量有限制(通常是每个线程最多16个本地引用)。如果超过这个限制,会导致
OutOfMemoryError
。 - 适用场景:适用于本地方法内临时使用的Java对象。
示例
JNIEXPORT void JNICALL Java_SomeClass_nativeMethod(JNIEnv *env, jobject obj) {
// 创建一个本地引用
jstring localStr = (*env)->NewStringUTF(env, "Hello, JNI!");
// 使用 localStr ...
// 当 nativeMethod 返回时,localStr 变得无效,不需要显式释放
}
在这个示例中,localStr
是一个本地引用,它在nativeMethod
调用期间有效。一旦nativeMethod
返回,localStr
引用的Java对象会被JVM自动回收。
2. 全局引用 (Global References)
定义
全局引用是在本地代码中创建的,可以在多个本地方法中共享,且在显式删除前一直有效。全局引用的生命周期可以跨越多个本地方法调用。
特点
- 生命周期长:全局引用在显式删除前一直有效,不会因本地方法返回而失效。
- 显式管理:需要通过JNI函数手动创建和删除全局引用。
- 不受限制:与本地引用不同,全局引用的数量不受JVM的默认限制,但过多使用全局引用会导致内存消耗增加。
- 适用场景:适用于在多个本地方法或跨线程使用的Java对象,或者需要在Java和本地代码之间保持长期引用的情况。
创建和删除全局引用
- 创建全局引用:使用
NewGlobalRef
方法。 - 删除全局引用:使用
DeleteGlobalRef
方法。
示例
jobject globalRef;
JNIEXPORT void JNICALL Java_SomeClass_createGlobalRef(JNIEnv *env, jobject obj) {
// 创建一个全局引用
globalRef = (*env)->NewGlobalRef(env, obj);
}
JNIEXPORT void JNICALL Java_SomeClass_useGlobalRef(JNIEnv *env, jobject obj) {
// 使用全局引用
if (globalRef != NULL) {
// 使用 globalRef ...
}
}
JNIEXPORT void JNICALL Java_SomeClass_deleteGlobalRef(JNIEnv *env, jobject obj) {
// 删除全局引用
if (globalRef != NULL) {
(*env)->DeleteGlobalRef(env, globalRef);
globalRef = NULL;
}
}
在这个示例中,globalRef
是一个全局引用。通过Java_SomeClass_createGlobalRef
创建全局引用,可以在后续的本地方法Java_SomeClass_useGlobalRef
中使用。最后,通过Java_SomeClass_deleteGlobalRef
删除全局引用,以避免内存泄漏。
3. 选择与使用建议
选择使用场景
- 本地引用:适用于本地方法内的临时对象和局部操作,不需要显式释放,JVM会自动处理。
- 全局引用:适用于需要在多个本地方法之间共享或保持对象引用的情况。由于生命周期较长,使用后必须显式删除以防止内存泄漏。
使用建议
- 避免滥用全局引用:全局引用不受数量限制,但过多的全局引用会导致内存使用增加,应合理使用。
- 及时删除全局引用:在不再需要全局引用时,应显式删除,以释放内存资源。
- 注意线程安全:全局引用可以跨线程使用,在多线程环境中应注意线程安全问题。
- 检查引用是否有效:在使用引用前,确保引用有效(例如全局引用未被删除)。
通过合理使用本地引用和全局引用,可以在JNI中有效地管理Java对象的生命周期和内存使用,避免内存泄漏和其他潜在问题。