JNI是什么?

JNI是什么?

JNI(Java Native Interface,Java本地接口)是Java平台自1.1版本起提供的标准编程接口,它是一套强大的编程框架,允许运行在Java虚拟机(JVM)中的Java代码与用C、C++等其他编程语言编写的本地代码进行交互。

核心特点

  • 功能扩展:允许Java程序调用本地代码,实现标准Java类库无法支持的功能
  • 性能优化:对于性能敏感的计算密集型任务(如图像处理、音视频编解码、复杂数学运算),本地代码通常比Java实现更高效
  • 代码复用:可以重用已有的C/C++库,无需用Java重新实现
  • 平台特定功能:使Java程序能够访问特定于操作系统或硬件的功能

工作原理

JNI提供了一套API,使得Java代码可以通过"native"关键字声明方法,这些方法在Java中没有实现,而是在C/C++等本地代码中实现。Java程序通过System.loadLibrary()加载包含这些本地方法的动态链接库(如Windows的.dll文件或Linux/macOS的.so文件)。

典型应用场景

  1. 性能优化:对计算密集型任务进行加速
  2. 硬件交互:直接访问硬件设备(如读卡器、打印机)
  3. 系统级功能:访问Java标准库无法提供的操作系统API
  4. 重用现有库:在Java应用中使用已有的C/C++库

注意事项

使用JNI会带来一些挑战:

  • 丧失Java的"一次编写,到处运行"的跨平台特性
  • 增加了代码的复杂性
  • 需要处理不同语言间的内存管理和数据类型转换
  • 调试难度增加

JNI是Java开发者"逃生门"("escape hatch"),在需要突破纯Java环境限制时提供必要的桥梁。

一个入门的例子

下面是一个 完整的、可运行的 JNI 简单示例,演示如何从 Java 调用 C 语言编写的本地方法。


🧩 示例目标

Java 程序调用一个 C 函数,该函数接收两个整数并返回它们的和。


第一步:编写 Java 类(HelloJNI.java)

java 复制代码
// HelloJNI.java
public class HelloJNI {
    // 声明一个 native 方法
    public native int add(int a, int b);

    // 加载本地库(动态链接库)
    static {
        System.loadLibrary("hello"); // 对应 libhello.so (Linux/macOS) 或 hello.dll (Windows)
    }

    public static void main(String[] args) {
        HelloJNI obj = new HelloJNI();
        int result = obj.add(3, 4);
        System.out.println("3 + 4 = " + result); // 预期输出:3 + 4 = 7
    }
}

第二步:编译 Java 文件并生成头文件

在命令行中执行:

bash 复制代码
# 编译 Java
javac HelloJNI.java

# 生成 JNI 头文件(JDK 8 及以下用 javah;JDK 10+ 用 javac -h)
# 如果你用的是 JDK 8 或更早:
javah HelloJNI

# 如果你用的是 JDK 10 或更高(推荐方式):
javac -h . HelloJNI.java

这会生成一个 HelloJNI.h 文件,内容类似:

c 复制代码
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif

/*
 * Class:     HelloJNI
 * Method:    add
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_HelloJNI_add
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

第三步:编写 C 实现(hello.c)

c 复制代码
// hello.c
#include <jni.h>
#include "HelloJNI.h"  // 刚才生成的头文件

JNIEXPORT jint JNICALL Java_HelloJNI_add
  (JNIEnv *env, jobject obj, jint a, jint b) {
    return a + b;
}

第四步:编译 C 代码为动态链接库

Linux / macOS:
bash 复制代码
# Linux
gcc -shared -fPIC -I"${JAVA_HOME}/include" -I"${JAVA_HOME}/include/linux" -o libhello.so hello.c

# macOS
gcc -shared -fPIC -I"${JAVA_HOME}/include" -I"${JAVA_HOME}/include/darwin" -o libhello.dylib hello.c

💡 请将 ${JAVA_HOME} 替换为你的 JDK 安装路径,例如 /usr/lib/jvm/java-11-openjdk。可通过 echo $JAVA_HOME 查看。

Windows(使用 MinGW 或 MSVC):
bash 复制代码
# 使用 MinGW 示例
gcc -shared -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -o hello.dll hello.c

生成的文件必须命名为:

  • Linux: libhello.so
  • macOS: libhello.dylib
  • Windows: hello.dll

并且放在 Java 能找到的路径下(如当前目录,或通过 -Djava.library.path=... 指定)。


第五步:运行 Java 程序

bash 复制代码
java HelloJNI

✅ 输出:

ini 复制代码
3 + 4 = 7

🔍 小结

这个例子展示了 JNI 的基本流程:

  1. Java 中声明 native 方法
  2. javac -h 生成 C 头文件
  3. 用 C 实现该方法
  4. 编译成动态库
  5. Java 加载库并调用

⚠️ 注意:JNI 编程涉及内存管理、异常处理、线程安全等复杂问题,实际项目中建议谨慎使用,或借助工具(如 JNA、GraalVM Native Image)简化开发。

JNI语法分析

上文的例子中,传统的C 实现代码,声明的关键字和"传统的 C 函数"不太一样,在此做一个简单的分析。

主要是因为 JNI 有一套自己的命名规则和类型系统,目的是让 Java 虚拟机(JVM)能正确地调用本地代码。

我们来逐行拆解,对比"传统 C"和"JNI C"的区别:


🔍 JNI 函数:

c 复制代码
JNIEXPORT jint JNICALL Java_HelloJNI_add
  (JNIEnv *env, jobject obj, jint a, jint b) {
    return a + b;
}

✅ 1. 函数名为什么这么长?Java_HelloJNI_add

这是 JNI 的强制命名规范,格式为:

复制代码
Java_包名_类名_方法名
  • 如果Java 类在默认包(没有 package 声明),就直接是 Java_类名_方法名
  • 所以 HelloJNI.add() → 对应 C 函数名:Java_HelloJNI_add

💡 这样 JVM 才知道:当 Java 调用 HelloJNI.add() 时,应该去动态库中找 Java_HelloJNI_add 这个符号。
📌 如果用了包,比如 package com.example;,那函数名就是:
Java_com_example_HelloJNI_add


✅ 2. JNIEXPORTJNICALL 是什么?

它们是 宏(macro),用于处理不同操作系统的编译差异。

  • JNIEXPORT:告诉编译器"这个函数要导出到动态库中",这样 Java 才能通过 System.loadLibrary 找到它。

    • 在 Windows 上,它可能展开为 __declspec(dllexport)
    • 在 Linux/macOS 上,通常为空或 __attribute__((visibility("default")))
  • JNICALL:指定函数的调用约定(calling convention),确保 JVM 和 C 函数传参方式一致。

    • 在 x86 Windows 上可能是 __stdcall
    • 其他平台通常为空

🧠 可以把它们理解为"为了让跨平台兼容而加的胶水宏",不用深究,照着写就行。


✅ 3. 参数列表为什么多了 JNIEnv *env, jobject obj

这是 JNI 的固定参数格式

参数 作用
JNIEnv *env 指向 JNI 环境的指针,通过它可以调用 JNI 提供的各种函数(比如创建 Java 对象、抛异常、访问字段等)
jobject obj 调用该 native 方法的 Java 对象实例(相当于 Java 中的 this)如果是 static native 方法,这里会是 jclass cls

🔸 Java 方法是:public native int add(int a, int b);(非 static)

所以第二个参数是 jobject obj(代表 this
🔸 如果是 public static native int add(int a, int b);

那 C 函数第二个参数就是 jclass cls(代表 HelloJNI.class


✅ 4. 为什么用 jint 而不是 int

JNI 定义了一套与平台无关的类型,确保在 32 位/64 位、不同操作系统上行为一致:

JNI 类型 对应 C 类型 对应 Java 类型
jint int32_t(通常是 int int
jboolean unsigned char boolean
jlong int64_t long
jfloat float float
jdouble double double
jstring jobject(特殊) String

所以 jint a, jint b 就是 Java 传过来的两个 int 参数。


✅ 对比:传统 C vs JNI C

场景 传统 C 函数 JNI C 函数
目的 独立运行或被其他 C 调用 被 JVM 调用
函数名 int add(int a, int b) Java_HelloJNI_add(...)
返回类型 int jint
参数 int a, int b JNIEnv*, jobject, jint, jint
导出控制 无需特殊处理 JNIEXPORT
调用约定 默认 JNICALL

✅ 总结一句话:

JNI 函数不是给"人"写的,是给"JVM"调用的,所以必须遵循它的命名、参数、类型规则,才能让 Java 和 C 正确"握手"。


如果只是想写一个普通的 C 函数做加法,可以写成:

c 复制代码
int add(int a, int b) { return a + b; }

JVM 不认识它 ,只有按 JNI 规范写的函数,Java 才能通过 native 关键字调用。


应用领域

JNI技术经典的应用在移动端底层/中间件开发 场景,常见于对性能、安全或硬件交互有较高要求的业务。这类场景的核心目标是:用 C++ 构建高性能、跨平台、可复用的原生能力,并通过 JNI 安全高效地暴露给上层 Android 应用(Java/Kotlin)使用


🎯 常见的业务场景包括:

1. 音视频处理
  • 场景:直播、短视频、视频会议、在线教育
  • C++ SDK 做什么
    • 音视频编解码(H.264/H.265、AAC)
    • 美颜滤镜、特效(基于 OpenGL ES / Vulkan)
    • 音频降噪、回声消除(AEC)、混响
    • 视频合成、裁剪、转码
  • 为什么用 C++:计算密集,Java 性能不足;已有成熟 C/C++ 库(如 FFmpeg、WebRTC)
2. 图像处理与计算机视觉(CV)
  • 场景:人脸识别、证件 OCR、AR 滤镜、智能拍照
  • C++ SDK 做什么
    • 调用 OpenCV、TensorFlow Lite、MNN 等推理引擎
    • 图像预处理(缩放、旋转、色彩空间转换)
    • 模型推理结果后处理
  • 为什么用 C++:模型推理和图像操作需极致性能;避免 Java GC 卡顿
3. 游戏引擎或图形渲染
  • 场景:手游、AR/VR 应用
  • C++ SDK 做什么
    • 封装 Unity/Unreal 引擎能力
    • 自研渲染管线(基于 Vulkan/Metal)
    • 物理模拟、动画系统
  • JNI 作用:让 Java 层控制游戏逻辑、UI 交互,核心渲染在 C++
4. 安全与加密
  • 场景:金融支付、数字版权保护(DRM)、隐私数据处理
  • C++ SDK 做什么
    • 实现加密算法(AES、RSA、国密 SM 系列)
    • 反调试、反逆向、代码混淆加固
    • 安全存储(绑定设备指纹)
  • 为什么用 C++:更难被逆向破解;可直接调用系统安全模块(如 Android Keystore)
5. 物联网(IoT)与硬件交互
  • 场景:智能硬件 App(如无人机、摄像头、医疗设备)
  • C++ SDK 做什么
    • 通过 USB/蓝牙/NFC 与设备通信
    • 解析私有协议、控制硬件指令
    • 实时数据流处理
  • JNI 作用:Java 层做 UI 和业务逻辑,C++ 层处理低延迟硬件通信
6. 跨平台 SDK 开发
  • 场景:一套 C++ 核心逻辑,同时支持 Android/iOS/Windows
  • C++ SDK 做什么
    • 业务核心逻辑(如网络协议、状态机、算法)
    • 平台无关的数据处理
  • JNI 作用:仅在 Android 端通过 JNI 暴露接口,iOS 则用 Objective-C++ 桥接

🔧 完整的技术栈通常包括:

类别 技术
核心语言 C++(11/14/17)、Java/Kotlin
JNI 相关 JNIEnv 使用、引用管理(Local/Global Ref)、异常处理、线程切换(AttachCurrentThread)
构建工具 CMake、NDK、Gradle(android.ndk abiFilters)
性能优化 内存池、零拷贝、CPU/GPU 负载分析(perf, systrace)
调试工具 addr2line、ndk-stack、GDB、LLDB
常用库 FFmpeg、OpenCV、Eigen、Protobuf、cURL、OpenSSL

💡 举个真实例子:

某短视频 App 的"一键美颜"功能:

  • 用户点击按钮(Kotlin UI)
  • 调用 BeautySDK.applyFilter(bitmap)
  • JNI 层将 Bitmap 的像素数据传给 C++
  • C++ 用 OpenGL 渲染美颜效果(磨皮、大眼)
  • 处理完的像素数据通过 JNI 回传,显示在屏幕上

**整个过程要求:低延迟( 在 Android 上构建"高性能原生能力底座",让上层应用既能享受 Java/Kotlin 的开发效率,又能获得 C++ 的极致性能与硬件控制力。

进程执行

请思考一个问题:从Java到C ++代码,是在一个进程里执行的吗?

答案是,从 Java 到 C++ 的 JNI 调用,是在同一个操作系统进程(process)中执行的

这是理解 JNI 工作机制的一个关键点。下面是解释:


✅ 核心

  • Java 代码 运行在 JVM(Java 虚拟机)
  • C++ 代码(通过 JNI) 也运行在 同一个 JVM 进程的地址空间内
  • 它们共享:
    • 同一个 进程 ID(PID)
    • 同一个 虚拟内存空间
    • 同一个 线程上下文(调用 JNI 的 Java 线程 = 执行 C++ 的线程)

📌 换句话说:JNI 不是跨进程通信(IPC),而是同进程内的跨语言函数调用


🔍 举个例子说明

假设你在 Android 上运行《王者荣耀》:

  • 整个 App 是一个 Linux 进程(比如 PID = 12345)
  • JVM(由 ART/Dalvik 实现)在这个进程中运行 Java/Kotlin 代码
  • 当你调用 native void updateGameLogic()
    • JVM 不启动新进程
    • 而是直接 跳转到已加载的 .so 动态库中的 C++ 函数
    • C++ 代码在 当前线程、当前进程 中执行
    • 执行完后 返回到 JVM 继续执行 Java 代码

整个过程就像:

text 复制代码
[Java Thread] → (JNI 跳转) → [C++ Function in same thread/process] → (返回) → [Java Thread]

🧠 为什么能这样?技术原理

  1. 动态库加载
    System.loadLibrary("game") 会把 libgame.so 加载到当前 JVM 进程的内存中 (类似 dlopen)。

  2. 函数符号绑定

    JVM 通过函数名(如 Java_com_tencent_Game_update)在 .so 中找到对应的 C++ 函数地址。

  3. 直接函数调用

    调用时,JVM 直接 call 指令跳转到该地址 ,就像调用普通 C 函数一样------没有进程切换开销

  4. JNIEnv 是线程局部的

    每个 Java 线程都有自己的 JNIEnv*,它指向 JVM 内部结构,C++ 通过它安全地访问 Java 对象。


⚠️ 注意:虽然同进程,但有"两个世界"

特性 Java 世界 C++ 世界
内存管理 自动 GC 手动 malloc/free
异常处理 try-catch 通常不用异常(或 SEH)
对象模型 Java 对象(堆上) C 结构体 / C++ 对象
线程 Java Thread pthread / std::thread(但必须通过 Attach 才能调 JNI)

❗ 危险点:如果 C++ 代码崩溃(如空指针解引用、栈溢出),整个 Android App 会闪退(SIGSEGV),因为它是主进程的一部分!


🆚 对比:什么情况下是"跨进程"?

技术 是否同进程 说明
JNI ✅ 同进程 Java ↔ C++ 在同一进程
Binder(AIDL) ❌ 跨进程 App ↔ 系统服务(如 CameraService)
Socket / Pipe ❌ 跨进程 两个独立 App 通信
NDK + 子进程 ❌ 跨进程 如果你手动 fork(),但 JNI 本身不涉及

💡 实际意义(对开发者)

  • 性能高:JNI 调用开销远小于跨进程通信(微秒级 vs 毫秒级)
  • 调试难 :Native 崩溃需要分析 tombstone 或使用 ndk-stack
  • 内存共享 :C++ 可以直接操作 Java 对象的内存(通过 GetByteArrayElements 等),但要小心 GC 移动对象
  • 线程安全:C++ 代码必须考虑多线程并发(因为多个 Java 线程可能同时调 JNI)

✅ 总结

JNI 是"同进程、跨语言"的函数调用机制。Java 和 C++ 代码运行在同一个 Android 应用进程中,共享内存和线程,只是编程模型和运行时环境不同。

这也是为什么 JNI 能实现高性能(如游戏、音视频),但也要求开发者对内存、线程、崩溃处理有更深的理解。

如果没有JNI?为什么需要JNI?

思考一个问题:JVM本身就是C++程序,它加载一个原生C语言库,应该很容易,JNI到底解决了哪些关键问题?

JVM 本身就是用 C++(以及部分 C)写成的系统级程序

无论是 Oracle HotSpot JVM、OpenJDK,还是 Android 的 ART(Android Runtime),它们的底层都是:

  • C++ 实现解释器、JIT 编译器、垃圾回收器、线程调度、内存管理等核心模块
  • 通过 动态链接机制 (如 dlopen / LoadLibrary)加载外部原生库(.so / .dll
  • 因此,让 JVM 调用一个 C/C++ 函数,在技术上确实"很容易"

🔧 那为什么还需要 JNI?直接调不行吗?

虽然 JVM 能加载 .so,但 "能加载" ≠ "能安全、正确、跨平台地调用"

JNI 的存在,是为了在 Java 的托管世界C/C++ 的原生世界 之间建立一座标准化、安全、可移植的桥梁

如果没有 JNI,会有什么问题?
问题 说明
1. 函数找不到 JVM 不知道哪个 C 函数对应哪个 Java native 方法
2. 参数传错 Java 的 intString、对象引用怎么变成 C 的参数?
3. 内存危险 C 直接访问 Java 对象内存?GC 可能正在移动对象!
4. 异常失控 C 抛异常?Java 怎么 catch?
5. 线程混乱 C 代码在哪个线程执行?能调回 Java 吗?
6. 平台差异 Windows 的 __stdcall vs Linux 的 calling convention

🌉 JNI 就是这座"标准化桥梁"

它通过以下机制解决上述问题:

1. 命名/注册规范
  • 自动映射:Java_包_类_方法 → C 函数
  • 或手动注册:RegisterNatives()(更高效,大厂常用)

C++中增加了方法重载,C++代码到C代码,也是需要方法映射的。

2. 类型封装
  • jint, jstring, jobject 等类型,屏蔽平台差异
  • 提供转换函数:GetStringUTFChars(), NewObject(), CallVoidMethod()...
3. 内存与 GC 协同
  • GetXXXCritical / ReleaseXXXCritical:临时锁定内存,防止 GC 移动
  • Local/Global Reference:管理 Java 对象在 Native 层的生命周期
4. 异常传播
  • C++ 可以调 ThrowNew() 抛出 Java 异常
  • Java 层能正常 try-catch
5. 线程绑定
  • 每个 Java 线程有唯一的 JNIEnv*
  • Native 线程可通过 AttachCurrentThread() 接入 JVM
6. 跨平台 ABI 兼容
  • JNICALLJNIEXPORT 宏自动适配不同 OS 的调用约定和导出规则

🧠 打个比方

JVM 就像一个讲"Java语"的国家,C++ 库是来自"C国"的专家。

虽然两国在同一块大陆(同一个进程),但语言不通、法律不同。
JNI 就是官方翻译 + 外交协议 + 安全通行证,确保双方合作既高效又不出乱子。

如果没有这套机制,JVM 虽然"技术上能加载 .so",但无法安全、可靠、可维护地集成原生代码。


💡 补充:Android ART 的特殊性

在 Android 上:

  • ART(取代了早期 Dalvik)也是用 C++ 写的
  • 它把 Java 字节码 AOT/JIT 编译成本地机器码
  • JNI 机制依然保留 ,因为:
    • 大量 SDK(如 OpenCV、FFmpeg)仍是 C/C++
    • 游戏引擎(Unity/UE)依赖 Native 逻辑
    • 系统 API 本身也通过 JNI 暴露(比如 android.graphics.Bitmap 底层是 Skia C++)

所以,即使整个 runtime 是 C++,JNI 仍然是连接"应用层 Java"和"底层能力 C++"的唯一标准通道


✅ 总结

JVM 作为 C++ 程序,加载 C 库在技术上很简单。
但 JNI 的价值不在于"能不能调",而在于"如何安全、高效、跨平台、可维护地调"。

它是一套经过 20+ 年验证的跨语言互操作规范,而不是简单的函数指针跳转。

相关推荐
贺biubiu8 小时前
2025 年终总结|总有那么一个人,会让你千里奔赴...
android·程序员·年终总结
野生的码农8 小时前
做好自己的份内工作,等着被裁
程序员·ai编程·vibecoding
Jing_Rainbow11 小时前
【 前端三剑客-37 /Lesson61(2025-12-09)】JavaScript 内存机制与执行原理详解🧠
前端·javascript·程序员
donecoding18 小时前
命令行与图形界面的复制哲学:从 `cp a b` 说起
程序员·命令行
AgentBuilder18 小时前
768维的谎言:SOTA视觉模型为何输给7个数字?
人工智能·程序员
大怪v1 天前
前端佬们!!AI大势已来,未来的上限取决你的独特气质!恭请批阅!!
前端·程序员·ai编程
程序员Agions2 天前
程序员武学修炼手册(二):进阶篇——小有所成,从能跑就行到知其所以然
前端·程序员
程序员Agions2 天前
程序员武学修炼手册(一):入门篇——初学乍练,从 Hello World 到能跑就行
程序员
PPPHUANG2 天前
Switch2Antigravity: 让 IntelliJ IDEA 与 Antigravity 无缝协作
程序员·intellij idea·vibecoding