一、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 必备工具

工具

用途

获取方式

JDK

编译运行Java代码

Oracle/OpenJDK官网

C/C++编译器

编译本地代码

Linux: GCC, Windows: MinGW, macOS: Clang

构建工具

管理编译过程

CMake 或 Make

2.2 验证环境

bash

# 检查JDK
java -version
javac -version

# 检查编译器(以GCC为例)
gcc --version

2.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
#endif

3.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/darwin

Windows(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 基本类型映射

Java类型

JNI类型

签名

描述

boolean

jboolean

Z

无符号8位

byte

jbyte

B

有符号8位

char

jchar

C

无符号16位

short

jshort

S

有符号16位

int

jint

I

有符号32位

long

jlong

J

有符号64位

float

jfloat

F

32位

double

jdouble

D

64位

void

void

V

无返回值

4.2 引用类型映射

Java类型

JNI类型

签名示例

String

jstring

Ljava/lang/String;

Object[]

jobjectArray

[Ljava/lang/Object;

int[]

jintArray

[I

任意类

jobject

L包名/类名;

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有三种引用类型:

引用类型

生命周期

使用场景

释放方式

局部引用

方法结束

临时使用

自动释放或DeleteLocalRef

全局引用

手动控制

跨方法/线程使用

DeleteGlobalRef

弱全局引用

手动控制

允许GC回收

DeleteWeakGlobalRef

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 性能建议

  1. 缓存方法ID和字段ID:在JNI_OnLoad中获取并缓存,避免重复查找

  2. 批量操作数组:使用GetArrayRegion代替逐个元素访问

  3. 减少跨边界调用:批量传递数据,减少Java↔C的切换次数

8.2 内存管理

  • 释放不再使用的局部引用:(*env)->DeleteLocalRef(env, localRef)

  • 全局引用必须手动释放,否则会造成内存泄漏

  • 使用GetStringUTFChars后必须调用ReleaseStringUTFChars

8.3 常见错误

错误信息

可能原因

解决方案

UnsatisfiedLinkError

库未找到或函数名不匹配

检查库路径和函数命名

NoSuchFieldError

字段ID错误

检查字段名和签名

NoSuchMethodError

方法ID错误

检查方法名和签名

程序崩溃

空指针或内存泄漏

添加空值检查和资源释放