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文件)。
典型应用场景
- 性能优化:对计算密集型任务进行加速
- 硬件交互:直接访问硬件设备(如读卡器、打印机)
- 系统级功能:访问Java标准库无法提供的操作系统API
- 重用现有库:在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 的基本流程:
- Java 中声明
native方法 - 用
javac -h生成 C 头文件 - 用 C 实现该方法
- 编译成动态库
- 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. JNIEXPORT 和 JNICALL 是什么?
它们是 宏(macro),用于处理不同操作系统的编译差异。
-
JNIEXPORT:告诉编译器"这个函数要导出到动态库中",这样 Java 才能通过System.loadLibrary找到它。- 在 Windows 上,它可能展开为
__declspec(dllexport) - 在 Linux/macOS 上,通常为空或
__attribute__((visibility("default")))
- 在 Windows 上,它可能展开为
-
JNICALL:指定函数的调用约定(calling convention),确保 JVM 和 C 函数传参方式一致。- 在 x86 Windows 上可能是
__stdcall - 其他平台通常为空
- 在 x86 Windows 上可能是
🧠 可以把它们理解为"为了让跨平台兼容而加的胶水宏",不用深究,照着写就行。
✅ 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]
🧠 为什么能这样?技术原理
-
动态库加载
System.loadLibrary("game")会把libgame.so加载到当前 JVM 进程的内存中 (类似dlopen)。 -
函数符号绑定
JVM 通过函数名(如
Java_com_tencent_Game_update)在.so中找到对应的 C++ 函数地址。 -
直接函数调用
调用时,JVM 直接 call 指令跳转到该地址 ,就像调用普通 C 函数一样------没有进程切换开销。
-
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 的 int、String、对象引用怎么变成 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 兼容
JNICALL、JNIEXPORT宏自动适配不同 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+ 年验证的跨语言互操作规范,而不是简单的函数指针跳转。