深入浅出 Android JNI (一)

1,095 阅读6分钟

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 平台设计。

    • 核心功能:

      1. 编译器 (如 Clang):  将你的 C/C++ 源代码编译成 Android 设备 CPU 能直接运行的机器码(通常是 .so 共享库文件)。
      2. 构建系统 (CMake/ndk-build):  帮助管理 Native 代码的编译、链接过程,并将其集成到 Android Studio 项目中。
      3. 交叉编译工具链:  支持为不同的 Android CPU 架构 (armeabi-v7a, arm64-v8a, x86, x86_64) 生成对应的库。
      4. 标准库支持:  提供部分 C/C++ 标准库 (如 libc++, libm) 和 Android 特有的 Native API (如 logjnigraphics)。

形象比喻:

  • 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() 的过程:

  1. Java 侧 - 发出请求:

    • 在 Java 类中声明一个 native 方法:

      public native int calculate(int a, int b);
      
    • 在 Java 代码的合适位置(通常是静态初始化块)加载包含该 native 方法实现的共享库:

      static {
          System.loadLibrary("my-math-lib"); // 加载 libmy-math-lib.so
      }
      
  2. JVM - 建立连接:

    • 当 Java 代码第一次调用 calculate(a, b) 时,JVM 会介入。
    • JVM 根据 JNI 函数命名规则(非常重要!通常是 Java_包名_类名_方法名)在已加载的库 libmy-math-lib.so 中查找对应的 C/C++ 函数。
  3. 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 类
  4. 返回结果 - 旅程结束:

    • Native 函数执行完毕,将结果(jint)返回。
    • JVM 接收结果,将其转换回 Java 的 int 类型,传递给调用 calculate() 的 Java 代码。

流程图简化版:

[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 对象引用 (如 jstringjobject) 需要按 JNI 规则管理(本地/全局引用)。获取的指针(如字符串、数组)用完后必须释放。
数据类型强类型,有明确的对象、接口、泛型概念。基础类型、结构体、指针、更贴近硬件。必须进行类型转换!  Java 的 int 在 JNI 是 jint,Java 的 String 是 jstring。转换规则是核心学习内容。
错误处理异常机制 (try-catch-throw)错误码 (return code / errno)  或 C++ 异常关键差异!  Native 中调用 Java 方法可能抛异常,必须用 JNI 函数 (ExceptionCheckExceptionOccurredExceptionClear) 检查和处理。C++ 异常绝不能直接穿越 JNI 边界。
对象生命周期对象由 GC 管理,不可控。对象生命周期完全由代码控制。在 Native 层持有 Java 对象引用时,需理解不同引用类型(本地、全局、弱全局)对 GC 的影响。
多线程线程由 JVM 管理,同步使用 synchronized 等。线程 (pthread 或 std::thread),同步用互斥锁等。JNIEnv* 是线程绑定的!  不能在非创建线程中使用。非 Java 创建的线程需先 Attach 到 JVM 获取 env,用完要 Detach

学习 JNI 的核心挑战之一,就是熟练掌握如何在两种截然不同的环境中安全、正确地传递数据、管理资源和处理错误