一、JNI概述
1.1 什么是JNI?
JNI(Java Native Interface,Java本地接口)是Java平台的一个标准特性,允许Java代码与用其他编程语言(主要是C和C++)编写的代码进行交互。简单来说,JNI是一座连接Java世界和Native世界的桥梁。
1.2 为什么需要JNI?
使用JNI的主要原因包括:
代码复用:复用现有的C/C++库,避免重复造轮子
性能优化:计算密集型任务(图形、视频、音频处理)使用C/C++实现性能更优
访问底层:Java无法直接访问的硬件、驱动等系统级功能
安全性:C/C++代码比Java字节码更难反编译
1.3 注意事项
使用JNI需要谨慎:
程序失去跨平台特性,不同系统需重新编译本地代码
本地代码的不当使用可能导致程序崩溃
增加了内存管理和错误处理的复杂性
二、开发环境配置
2.1 必备工具
2.2 验证环境
bash
# 检查JDK
java -version
javac -version
# 检查编译器(以GCC为例)
gcc --version2.3 Android NDK环境(Android开发专用)
如果是Android开发,还需要安装NDK(Native Development Kit):
NDK:Android提供的工具集,用于编译C/C++代码生成.so动态库
CMake或ndk-build:构建脚本工具
三、JNI快速入门:Hello World
让我们通过一个完整的例子,从零开始体验JNI开发流程。
3.1 编写Java代码
创建HelloJNI.java:
java
public class HelloJNI {
// 1. 声明native方法
public native void sayHello();
// 2. 加载本地库
static {
System.loadLibrary("hello"); // 加载libhello.so(Linux)或hello.dll(Windows)
}
public static void main(String[] args) {
new HelloJNI().sayHello();
System.out.println("Java: 程序执行完毕");
}
}3.2 编译并生成头文件
bash
# 编译Java文件
javac HelloJNI.java
# 生成C/C++头文件(JDK 10+使用此命令)
javac -h . HelloJNI.java
# 如果是JDK 8及更早版本,使用:
# javah HelloJNI执行后会生成HelloJNI.h文件,内容类似:
c
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_HelloJNI_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif3.3 实现C代码
创建HelloJNI.c:
c
#include <stdio.h>
#include <jni.h>
#include "HelloJNI.h"
// 函数名必须严格遵守命名规则:Java_包名_类名_方法名
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj) {
printf("Hello from C!\n");
printf("C: 这是被Java调用的本地方法\n");
}3.4 编译生成动态库
Linux / macOS:
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
# Linux
gcc -shared -fPIC -o libhello.so HelloJNI.c \
-I${JAVA_HOME}/include \
-I${JAVA_HOME}/include/linux
# macOS
gcc -shared -fPIC -o libhello.dylib HelloJNI.c \
-I${JAVA_HOME}/include \
-I${JAVA_HOME}/include/darwinWindows(MinGW):
gcc -shared -o hello.dll HelloJNI.c \
-I"%JAVA_HOME%\include" \
-I"%JAVA_HOME%\include\win32"3.5 运行程序
# 指定库路径并运行
java -Djava.library.path=. HelloJNI
# 预期输出:
# Hello from C!
# C: 这是被Java调用的本地方法
# Java: 程序执行完毕3.6 常见错误解决
如果遇到java.lang.UnsatisfiedLinkError,可能原因:
动态库文件名不正确(Linux必须是
libxxx.so格式)库不在搜索路径中
System.loadLibrary()参数错误(不要带lib前缀和.so后缀)
四、数据类型映射
4.1 基本类型映射
4.2 引用类型映射
4.3 方法签名
方法签名格式:(参数类型)返回值类型
示例:
// Java方法
public String concat(String a, int b, int[] c)
// 对应签名
"(Ljava/lang/String;I[I)Ljava/lang/String;"签名速查表:
(I)V→ 参数int,返回void(II)I→ 两个int参数,返回int(Ljava/lang/String;)Z→ String参数,返回boolean([B)V→ byte数组参数,无返回值
五、深入JNI编程
5.1 处理字符串
C代码中操作Java字符串需要使用JNI函数:
JNIEXPORT jstring JNICALL Java_StringProcessor_process(JNIEnv *env, jobject obj, jstring input) {
// 获取UTF-8格式的C字符串
jboolean isCopy;
const char *cInput = (*env)->GetStringUTFChars(env, input, &isCopy);
if (cInput == NULL) {
return NULL; // 内存分配失败
}
// 处理字符串
char cOutput[256];
sprintf(cOutput, "处理结果: %s", cInput);
// 释放资源
(*env)->ReleaseStringUTFChars(env, input, cInput);
// 创建Java字符串并返回
return (*env)->NewStringUTF(env, cOutput);
}常用字符串函数:
GetStringUTFChars:获取UTF-8字符串ReleaseStringUTFChars:释放字符串NewStringUTF:创建新的Java字符串GetStringLength:获取Unicode字符串长度GetStringUTFLength:获取UTF-8字符串长度
5.2 处理数组
JNIEXPORT jint JNICALL Java_ArrayProcessor_sum(JNIEnv *env, jobject obj, jintArray array) {
// 获取数组长度
jsize len = (*env)->GetArrayLength(env, array);
// 获取数组元素指针
jint *elements = (*env)->GetIntArrayElements(env, array, NULL);
if (elements == NULL) {
return 0;
}
// 计算总和
jint sum = 0;
for (int i = 0; i < len; i++) {
sum += elements[i];
}
// 释放数组元素
(*env)->ReleaseIntArrayElements(env, array, elements, JNI_ABORT);
return sum;
}数组操作模式:
GetIntArrayElements:获取int数组指针ReleaseIntArrayElements:释放数组GetIntArrayRegion:复制数组区域到C缓冲区SetIntArrayRegion:从C缓冲区设置数组区域
5.3 访问Java字段
// Java类定义
public class Person {
public String name;
public int age;
public native void updateInfo();
}c
JNIEXPORT void JNICALL Java_Person_updateInfo(JNIEnv *env, jobject obj) {
// 获取对象的类
jclass clazz = (*env)->GetObjectClass(env, obj);
// 获取字段ID
jfieldID nameField = (*env)->GetFieldID(env, clazz, "name", "Ljava/lang/String;");
jfieldID ageField = (*env)->GetFieldID(env, clazz, "age", "I");
// 修改age字段
(*env)->SetIntField(env, obj, ageField, 30);
// 修改name字段
jstring newName = (*env)->NewStringUTF(env, "李四");
(*env)->SetObjectField(env, obj, nameField, newName);
}5.4 调用Java方法
JNIEXPORT void JNICALL Java_Callback_callJavaMethod(JNIEnv *env, jobject obj) {
jclass clazz = (*env)->GetObjectClass(env, obj);
// 获取方法ID(方法名,签名)
jmethodID methodId = (*env)->GetMethodID(env, clazz, "onNativeCallback",
"(Ljava/lang/String;)V");
if (methodId == NULL) {
return; // 方法不存在
}
// 创建参数
jstring message = (*env)->NewStringUTF(env, "来自C层的调用");
// 调用Java方法
(*env)->CallVoidMethod(env, obj, methodId, message);
}5.5 异常处理
JNI中的异常处理需要特别注意,C/C++不支持Java的try-catch机制:
JNIEXPORT void JNICALL Java_ExceptionHandler_riskyOperation(JNIEnv *env, jobject obj) {
// 调用可能抛出异常的Java方法
jclass clazz = (*env)->GetObjectClass(env, obj);
jmethodID methodId = (*env)->GetMethodID(env, clazz, "dangerousMethod", "()V");
(*env)->CallVoidMethod(env, obj, methodId);
// 检查是否发生异常
jthrowable exc = (*env)->ExceptionOccurred(env);
if (exc != NULL) {
// 清除异常
(*env)->ExceptionClear(env);
// 处理异常
printf("捕获到异常,已处理\n");
}
}六、高级主题
6.1 动态注册
相比静态注册(函数名必须严格遵守Java_包名_类名_方法名规则),动态注册更灵活:
#include <jni.h>
#include <stdio.h>
// 实现函数
jstring nativeGetMessage(JNIEnv *env, jobject obj) {
return (*env)->NewStringUTF(env, "Hello from dynamic registration");
}
jint nativeAdd(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b;
}
// 方法映射表
static JNINativeMethod methods[] = {
{"getMessage", "()Ljava/lang/String;", (void*)&nativeGetMessage},
{"add", "(II)I", (void*)&nativeAdd},
};
// JNI_OnLoad是动态库加载时的入口函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass clazz = (*env)->FindClass(env, "com/example/NativeLib");
if (clazz == NULL) {
return JNI_ERR;
}
// 注册方法
if ((*env)->RegisterNatives(env, clazz, methods,
sizeof(methods)/sizeof(methods[0])) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}6.2 全局引用
JNI有三种引用类型:
jclass localClass = (*env)->FindClass(env, "java/lang/String");
jclass globalClass = (*env)->NewGlobalRef(env, localClass);
// 使用globalClass...
// 不再使用时释放
(*env)->DeleteGlobalRef(env, globalClass);6.3 多线程注意事项
JNIEnv只在创建它的线程中有效,不能跨线程使用:
// 从子线程中获取JNIEnv
void *thread_func(void *arg) {
JavaVM *jvm = (JavaVM*)arg;
JNIEnv *env;
// 附加到当前线程
(*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
// 使用env进行JNI调用...
// 分离线程
(*jvm)->DetachCurrentThread(jvm);
return NULL;
}七、Android中的JNI
7.1 Android项目结构
app/
├── src/main/
│ ├── java/ # Java源代码
│ ├── cpp/ # C/C++源代码(CMake方式)
│ └── jni/ # C/C++源代码(ndk-build方式)
└── build.gradle # 配置文件7.2 build.gradle配置(CMake方式)
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++11"
abiFilters "armeabi-v7a", "arm64-v8a"
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}7.3 CMakeLists.txt示例
cmake_minimum_required(VERSION 3.4.1)
add_library(native-lib SHARED
src/main/cpp/native-lib.cpp)
find_library(log-lib log)
target_link_libraries(native-lib ${log-lib})八、最佳实践与常见问题
8.1 性能建议
缓存方法ID和字段ID:在
JNI_OnLoad中获取并缓存,避免重复查找批量操作数组:使用
GetArrayRegion代替逐个元素访问减少跨边界调用:批量传递数据,减少Java↔C的切换次数
8.2 内存管理
释放不再使用的局部引用:
(*env)->DeleteLocalRef(env, localRef)全局引用必须手动释放,否则会造成内存泄漏
使用
GetStringUTFChars后必须调用ReleaseStringUTFChars
评论