1. 什么是 JNI?它为何而生?
-
JNI 的全称是 Java Native Interface (Java 本地接口)。
-
核心使命:打破语言壁垒! 它定义了一套标准规范,使得运行在 Java 虚拟机 (JVM) 中的 Java/Kotlin 代码 能够与 本地代码 (Native Code,通常是 C 或 C++) 进行安全、高效的双向通信。
-
为什么需要 JNI?纯 Java 不够香吗?
- 性能为王: 对计算密集型任务(如图像处理、物理仿真、音视频编解码),C/C++ 通常能提供远超 Java 的极致性能。
- 硬件操控: 直接操作特定硬件(如传感器底层、GPU 加速)往往需要 C/C++ 的底层访问能力。
- 遗产代码复用: 庞大的现存 C/C++ 库(如 OpenCV, FFmpeg, 加密算法库)是宝贵的财富,JNI 让你无需重写,直接集成。
- 平台特定 API: 访问 Android NDK 提供的、Java 层未暴露的底层系统功能。
- 安全敏感计算: 将核心算法或密钥操作放在更难以逆向的 Native 层(需结合其他安全措施)。
一句话总结: JNI 是为了弥补 Java 在性能、硬件操作、复用已有库方面的不足,让开发者能在享受 Java 开发便利的同时,调用 Native 代码的洪荒之力。
2. JNI 在 Android 中的定位 (与 NDK 的关系)
初学时常容易混淆 JNI 和 NDK,它们紧密相关但职责不同:
-
JNI (Java Native Interface):
- 它是一套接口规范、一套协议。 定义了 Java 如何调用 Native 函数 以及 Native 代码如何回调 Java 的规则(函数命名、参数传递、数据类型映射、内存管理、异常处理等)。
- 与平台无关: JNI 规范本身是 Java 平台的标准,不仅适用于 Android,也适用于其他 Java 环境。
-
NDK (Native Development Kit):
-
它是一套开发工具链。 由 Google 提供,专为 Android 平台设计。
-
核心功能:
- 编译器 (如 Clang): 将你的 C/C++ 源代码编译成 Android 设备 CPU 能直接运行的机器码(通常是
.so共享库文件)。 - 构建系统 (CMake/ndk-build): 帮助管理 Native 代码的编译、链接过程,并将其集成到 Android Studio 项目中。
- 交叉编译工具链: 支持为不同的 Android CPU 架构 (armeabi-v7a, arm64-v8a, x86, x86_64) 生成对应的库。
- 标准库支持: 提供部分 C/C++ 标准库 (如 libc++, libm) 和 Android 特有的 Native API (如
log,jnigraphics)。
- 编译器 (如 Clang): 将你的 C/C++ 源代码编译成 Android 设备 CPU 能直接运行的机器码(通常是
-
形象比喻:
- JNI 是“通信协议” :就像两国元首通话时使用的翻译规则和外交礼仪手册。
- NDK 是“通信设备和翻译官” :提供电话线路(工具链)、同声传译(编译器)、并确保翻译符合外交规范(生成符合 Android 要求的
.so文件)。 - 你的 C/C++ 代码是“本国发言人” :需要遵循协议(JNI 规范),通过设备(NDK)与对方(Java)交流。
- 你的 Java 代码是“外国元首” :声明通话需求(
native方法),加载通信渠道(System.loadLibrary)。
关键点: 在 Android 中开发 JNI 程序,你 必须 使用 NDK 来编译构建你的 C/C++ 代码,生成的 .so 库通过 JNI 规范与 Java 层交互。
3. JNI 工作原理全景图 (一次调用的旅程)
想象一下 Java 代码要调用一个 C 函数 calculate() 的过程:
-
Java 侧 - 发出请求:
-
在 Java 类中声明一个
native方法:public native int calculate(int a, int b); -
在 Java 代码的合适位置(通常是静态初始化块)加载包含该
native方法实现的共享库:static { System.loadLibrary("my-math-lib"); // 加载 libmy-math-lib.so }
-
-
JVM - 建立连接:
- 当 Java 代码第一次调用
calculate(a, b)时,JVM 会介入。 - JVM 根据 JNI 函数命名规则(非常重要!通常是
Java_包名_类名_方法名)在已加载的库libmy-math-lib.so中查找对应的 C/C++ 函数。
- 当 Java 代码第一次调用
-
Native 侧 - 执行任务:
-
JVM 找到函数后(例如
Java_com_example_myapp_MathUtils_calculate),就会调用它。 -
这个 C/C++ 函数必须遵循特定的 签名:
#include <jni.h> // 必须包含 JNI 头文件 JNIEXPORT jint JNICALL Java_com_example_myapp_MathUtils_calculate(JNIEnv *env, jobject thiz, jint a, jint b) { // 1. 使用 env 指针访问 JNI 函数 // 2. thiz 指向调用此 native 方法的 Java 对象 (如果是静态方法则为 jclass) // 3. a, b 是从 Java 传过来的 jint (对应 Java 的 int) int result = a + b; // 执行计算 (简单示例) return (jint)result; // 将结果 (C int) 转换为 jint 返回给 Java } -
关键参数解析:
JNIEnv *env: 这是 最重要的参数! 它是一个指向 JNI 环境函数表的指针。几乎所有 JNI 函数(如创建对象、调用方法、操作数组)都需要通过这个env指针来调用。它提供了与 Java 世界交互的 全部能力。注意:env指针是线程相关的!jobject thiz: 如果这个native方法是一个 实例方法,那么thiz就指向调用该方法的 Java 对象实例。如果native方法是 静态方法,则这个参数是jclass类型,指向 声明该方法的 Java 类。
-
-
返回结果 - 旅程结束:
- Native 函数执行完毕,将结果(
jint)返回。 - JVM 接收结果,将其转换回 Java 的
int类型,传递给调用calculate()的 Java 代码。
- Native 函数执行完毕,将结果(
流程图简化版:
[Java] `native int calculate(a, b);` -> [JVM] 查找 `libmy-math-lib.so` 中的 `Java_..._calculate` -> [C++] `Java_..._calculate(env, thiz, a, b) { ... return result; }` -> [JVM] 转换结果 -> [Java] 得到 int result
4. 跨越鸿沟:Java 与 C/C++ 的思维转换
当你在 JNI 层穿梭于 Java 和 C/C++ 之间时,必须时刻牢记它们核心机制的差异:
| 特性 | Java / Kotlin (JVM 世界) | C/C++ (Native 世界) | JNI 层注意事项 |
|---|---|---|---|
| 内存管理 | 自动垃圾回收 (GC) 。开发者无需手动释放对象内存。 | 手动管理。malloc/free 或 new/delete 必须成对出现。 | 极易出错点! Native 层创建的 Java 对象引用 (如 jstring, jobject) 需要按 JNI 规则管理(本地/全局引用)。获取的指针(如字符串、数组)用完后必须释放。 |
| 数据类型 | 强类型,有明确的对象、接口、泛型概念。 | 基础类型、结构体、指针、更贴近硬件。 | 必须进行类型转换! Java 的 int 在 JNI 是 jint,Java 的 String 是 jstring。转换规则是核心学习内容。 |
| 错误处理 | 异常机制 (try-catch-throw) 。 | 错误码 (return code / errno) 或 C++ 异常。 | 关键差异! Native 中调用 Java 方法可能抛异常,必须用 JNI 函数 (ExceptionCheck, ExceptionOccurred, ExceptionClear) 检查和处理。C++ 异常绝不能直接穿越 JNI 边界。 |
| 对象生命周期 | 对象由 GC 管理,不可控。 | 对象生命周期完全由代码控制。 | 在 Native 层持有 Java 对象引用时,需理解不同引用类型(本地、全局、弱全局)对 GC 的影响。 |
| 多线程 | 线程由 JVM 管理,同步使用 synchronized 等。 | 线程 (pthread 或 std::thread),同步用互斥锁等。 | JNIEnv* 是线程绑定的! 不能在非创建线程中使用。非 Java 创建的线程需先 Attach 到 JVM 获取 env,用完要 Detach。 |
学习 JNI 的核心挑战之一,就是熟练掌握如何在两种截然不同的环境中安全、正确地传递数据、管理资源和处理错误