JNI开发

1、JNI静态与动态注册

在Java Native Interface (JNI) 中,注册指的是将Java方法与其本地实现进行关联。JNI提供了两种主要的注册方式:静态注册动态注册。这两种方式各有优缺点和使用场景。下面对这两种注册方式进行详细说明。

1. 静态注册 (Static Registration)

定义

静态注册是指通过JNI生成的头文件中的函数名称来实现Java方法与本地实现的绑定。函数名称遵循特定的命名规则,以确保Java虚拟机能够正确地找到对应的本地实现。

特点

  • 命名约定:本地函数的名称必须遵循特定的命名规则:Java_类全限定名_方法名
  • 代码自动生成:使用javac -hjavah工具时,会自动生成带有正确函数名称的头文件。
  • 简单易用:对于大多数应用场景来说,静态注册足够且容易实现。

实现步骤

  1. 编写Java代码,声明本地方法
  2. 使用javac -hjavah生成头文件。头文件中包含了本地方法的函数声明。
  3. 在本地代码中实现头文件中声明的函数

示例

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方法与本地实现关联。与静态注册不同,动态注册不要求本地函数名遵循特定命名规则。

特点

  • 灵活性:不依赖于固定的命名规则,开发者可以自行决定函数名称。
  • 集中管理:所有的注册都可以集中在一个地方进行,便于管理。
  • 复杂性:实现比静态注册复杂,需要编写额外的注册代码。

实现步骤

  1. 编写Java代码,声明本地方法
  2. 实现JNI_OnLoad函数,并在其中使用JNIEnvRegisterNatives方法注册本地方法。
  3. 在本地代码中定义本地方法实现

示例

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. 常用类型签名
  • 基本类型

    • Zboolean
    • Bbyte
    • Cchar
    • Sshort
    • Iint
    • Jlong
    • Ffloat
    • Ddouble
    • Vvoid(仅用于返回类型)
  • 引用类型

    • 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会自动处理。
  • 全局引用:适用于需要在多个本地方法之间共享或保持对象引用的情况。由于生命周期较长,使用后必须显式删除以防止内存泄漏。

使用建议

  1. 避免滥用全局引用:全局引用不受数量限制,但过多的全局引用会导致内存使用增加,应合理使用。
  2. 及时删除全局引用:在不再需要全局引用时,应显式删除,以释放内存资源。
  3. 注意线程安全:全局引用可以跨线程使用,在多线程环境中应注意线程安全问题。
  4. 检查引用是否有效:在使用引用前,确保引用有效(例如全局引用未被删除)。

通过合理使用本地引用和全局引用,可以在JNI中有效地管理Java对象的生命周期和内存使用,避免内存泄漏和其他潜在问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值