杰瑞科技汇

Android如何通过JNI调用本地库?

Android JNI 教程:从入门到实践

目录

  1. 什么是 JNI?为什么需要它?
  2. 环境准备
  3. JNI 基础知识详解
    • JNI 函数命名规则
    • 基本数据类型与引用类型
    • 字符串处理 (jstring)
    • 数组处理 (jarray)
    • 方法调用 (Call<Type>Method)
    • 异常处理
  4. 实战演练:一个完整的 JNI 项目
    • 第一步:创建 Android 项目并配置 CMake
    • 第二步:在 Java/Kotlin 中声明 native 方法
    • 第三步:使用 javahjavac -h 生成 C/C++ 头文件
    • 第四步:实现 C/C++ 代码
    • 第五步:配置 CMakeLists.txt 以编译 C/C++ 代码
    • 第六步:加载并调用 native 方法
  5. 进阶主题
    • 在 C/C++ 中创建和操作 Java 对象
    • 处理回调
    • NDK 与 ABI (Application Binary Interface)
  6. 常见问题与最佳实践
  7. 总结与学习资源

什么是 JNI?为什么需要它?

JNI (Java Native Interface),即 Java 本地接口,是 Java 平台的一部分,它允许 Java 代码和其他语言(主要是 C、C++ 和汇编)编写的代码进行交互。

Android如何通过JNI调用本地库?-图1
(图片来源网络,侵删)

为什么需要 JNI?

  • 性能优化:对于计算密集型任务(如图像处理、物理模拟、游戏引擎),用 C/C++ 实现会比纯 Java/Kotlin 快得多。
  • 复用现有库:项目中可能已经存在成熟、高效的 C/C++ 库(如 OpenCV、FFmpeg),通过 JNI 可以在 Android 中直接调用它们,而无需重新开发。
  • 访问底层硬件:某些硬件功能可能没有提供 Java API,但可以通过 C/C++ 直接访问硬件驱动。
  • 平台特定代码:当需要实现平台特有的功能时,可以使用 JNI 来编写平台相关的代码。

核心思想:JNI 是一座桥梁,一头连着 Java 虚拟机,另一头连着本地(Native)代码。


环境准备

在开始之前,请确保你的开发环境已经配置好:

  1. 安装 Android Studio:最新版本即可。
  2. 配置 NDK
    • 打开 Android Studio,进入 Tools -> SDK Manager
    • SDK Tools 标签页中,勾选 NDK (Side by side)CMake
    • 点击 Apply 进行安装。
  3. 创建或打开一个支持 C++ 的项目
    • 创建新项目时,选择 Native C++ 模板,它会自动帮你配置好 CMake 和基本结构。
    • 如果已有项目,可以通过 Tools -> SDK Manager -> NDK (Side by side) 下载后,手动配置。

JNI 基础知识详解

JNI 函数命名规则

当你从 C/C++ 调用一个 Java 方法时,JVM 需要找到这个方法的实现,JNI 使用一套严格的命名规则来定位方法,这个函数名由以下几部分组成:

Android如何通过JNI调用本地库?-图2
(图片来源网络,侵删)

Java_ + [包名] + [类名] + _ + [方法名] + __(参数签名)

  • 包名和类名:用下划线 _ 替换点 。
  • 方法名:保持不变。
  • 参数签名:描述了方法参数和返回值的类型。

示例: Java 方法定义:com.example.myapp.MainActivity.stringFromJNI() 对应的 JNI 函数名可能是:Java_com_example_myapp_MainActivity_stringFromJNI(JNIEnv*, jobject)

基本数据类型与引用类型

Java 类型 JNI 类型 描述
byte jbyte 8位有符号整数
short jshort 16位有符号整数
int jint 32位有符号整数
long jlong 64位有符号整数
float jfloat 32位浮点数
double jdouble 64位浮点数
char jchar 16位 Unicode 字符
boolean jboolean 8位布尔值 (true=1, false=0)
Object jobject 任何 Java 对象
Class jclass java.lang.Class 对象
String jstring java.lang.String 对象
Object[] jobjectArray 任何 Java 对象数组

字符串处理 (jstring)

Java 的 String 和 C/C++ 的 char* 是不同的,必须使用 JNI 提供的函数进行转换。

*jstring 转换为 `const char` (UTF-8)**:

const char* c_str = env->GetStringUTFChars(j_str, nullptr);
if (c_str == nullptr) {
    // 处理内存分配失败
    return;
}
// ... 使用 c_str
// 使用完毕后,必须释放内存
env->ReleaseStringUTFChars(j_str, c_str);

*从 `const char转换为jstring`**:

const char* c_str = "Hello from C++";
jstring j_str = env->NewStringUTF(c_str);
// ... 将 j_str 返回给 Java
// 注意:这个 j_string 需要在 Java 或 C++ 中通过 DeleteLocalRef 释放

数组处理 (jarray)

处理数组同样需要特殊函数。

获取数组元素指针

// 对于 jintArray
jintArray java_array = ...;
jint* native_array = env->GetIntArrayElements(java_array, nullptr);
if (native_array == nullptr) {
    // 处理错误
    return;
}
// ... 可以像操作普通 C 数组一样操作 native_array
// 修改第一个元素
native_array[0] = 100;
// 释放数组
// mode: 0 (拷回并释放), JNI_ABORT (仅释放,不拷回)
env->ReleaseIntArrayElements(java_array, native_array, 0);

方法调用 (Call<Type>Method)

在 C/C++ 中调用 Java 方法,需要通过 JNIEnv 指针。

  1. 获取类对象jclass clazz = env->GetObjectClass(obj);
  2. 获取方法IDjmethodID mid = env->GetMethodID(clazz, "methodName", "(Ljava/lang/String;)V"); (签名是 参数类型)返回值类型)
  3. 调用方法env->CallVoidMethod(obj, mid, j_str);

Call<Type>Method 系列函数:

  • CallVoidMethod: 返回值为 void
  • CallIntMethod: 返回值为 int
  • CallObjectMethod: 返回值为 Object
  • CallStatic<Type>Method: 调用静态方法

异常处理

JNI 调用可能会抛出 Java 异常,C/C++ 代码必须检查并处理这些异常,否则可能会导致程序崩溃。

// 在可能抛出异常的 JNI 调用后检查
if (env->ExceptionCheck()) {
    // 打印异常信息到 Logcat
    env->ExceptionDescribe();
    // 清除异常,否则后续 JNI 调用会失败
    env->ExceptionClear();
    // ... 进行错误处理
}

实战演练:一个完整的 JNI 项目

我们将创建一个简单的例子,让 C++ 代码返回一个字符串给 Java/Kotlin。

第一步:创建 Android 项目并配置 CMake

  1. 新建一个 Android 项目,选择 Native C++ 模板。
  2. Android Studio 会自动创建以下文件:
    • app/src/main/cpp/native-lib.cpp: C++ 源文件。
    • app/build.gradle: 包含 externalNativeBuild 配置。
    • app/CMakeLists.txt: CMake 构建脚本。

第二步:在 Java/Kotlin 中声明 native 方法

打开 MainActivity.java (或 MainActivity.kt),添加一个 native 方法。

Java (MainActivity.java):

package com.example.myapp;
public class MainActivity extends AppCompatActivity {
    // 加载包含 native 方法的库
    static {
        System.loadLibrary("native-lib");
    }
    // 声明一个 native 方法
    public native String stringFromJNI();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.sample_text);
        // 调用 native 方法并显示结果
        tv.setText(stringFromJNI());
    }
}

Kotlin (MainActivity.kt):

package com.example.myapp
class MainActivity : AppCompatActivity() {
    // 加载库
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
    // 声明一个 native 方法
    external fun stringFromJNI(): String
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val tv = findViewById<TextView>(R.id.sample_text)
        // 调用 native 方法并显示结果
        tv.text = stringFromJNI()
    }
}

第三步:生成 C/C++ 头文件

这是连接 Java 和 C/C++ 的关键一步,我们需要根据 Java 方法的声明,生成一个 C/C++ 头文件,其中包含了 JNI 函数的原型。

打开 Android Studio 的终端 (Terminal),运行以下命令:

# 进入 java 目录
cd app/src/main/java
# 使用 javac 编译 Java 文件并生成头文件
# -h 参数指定头文件输出目录
javac -cp . -d ../.. -h ../.. com/example/myapp/MainActivity.java

执行后,你会在 app/src/main/ 目录下找到一个名为 com_example_myapp_MainActivity.h 的头文件。 (com_example_myapp_MainActivity.h)**:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_myapp_MainActivity */
#ifndef _Included_com_example_myapp_MainActivity
#define _Included_com_example_myapp_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_myapp_MainActivity
 * Method:    stringFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_myapp_MainActivity_stringFromJNI
  (JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif

第四步:实现 C/C++ 代码

打开 app/src/main/cpp/native-lib.cpp,包含并实现刚刚生成的头文件中的函数。

native-lib.cpp:

#include <jni.h>
#include <string>
#include <android/log.h> // 用于在 Logcat 中打印日志
#define LOG_TAG "JNI_TAG"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// 实现 Java_com_example_myapp_MainActivity_stringFromJNI 函数
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    // 创建一个 C++ 字符串
    std::string hello = "Hello from C++";
    // 将 C++ 字符串转换为 jstring 并返回
    return env->NewStringUTF(hello.c_str());
}
  • extern "C":告诉 C++ 编译器使用 C 语言的链接方式,这样函数名不会被 C++ 的 name mangling (名字修饰) 改变,从而能被 JNI 正确找到。
  • JNIEnv* env:指向 JNI 环境的指针,是所有 JNI 函数的第一个参数。
  • jobject this:代表调用这个 native 方法的 Java 对象实例(非静态方法),如果是静态方法,这里会是 jclass clazz

第五步:配置 CMakeLists.txt

确保 app/CMakeLists.txt 文件能够找到并编译你的 native 代码。

CMakeLists.txt:

# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.18.1)
# project 定义项目名称和语言
project("native-lib")
# 添加 C++ 标准
set(CMAKE_CXX_STANDARD 17)
# 查找 log 库
find_library(log-lib log)
# 添加你的 native-lib 库
# 将 native-lib.cpp 添加到库中
add_library(
        native-lib        # 库名
        SHARED            # 共享库 (.so)
        native-lib.cpp)   # 源文件
# 链接 log 库到 native-lib
target_link_libraries(
        native-lib
        ${log-lib})
  • add_library: 定义了一个名为 native-lib 的共享库。
  • target_link_libraries: 将 log 库链接到我们的 native-lib,这样我们就可以在 C++ 代码中使用 __android_log_print 了。

第六步:加载并调用 native 方法

这一步在第二步中已经完成了。System.loadLibrary("native-lib") 会加载 libnative-lib.so 库。external fun stringFromJNI() 的实现会自动链接到 Java_com_example_myapp_MainActivity_stringFromJNI 函数。

运行你的 App,如果一切顺利,你应该能在屏幕上看到 "Hello from C++"。


进阶主题

在 C/C++ 中创建和操作 Java 对象

除了调用方法,你还可以在 C/C++ 中创建新的 Java 对象。

  1. 找到构造函数的 IDGetMethodID(clazz, "<init>", "()V"); (构造函数名永远是 <init>)
  2. 创建对象jobject obj = env->NewObject(clazz, constructor_id);
  3. 访问对象的字段:使用 GetFieldIDSetFieldID

处理回调

这是一个非常常见的场景:Java/Kotlin 设置一个回调,C/C++ 在某个事件发生时调用它。

  1. 在 Java 中定义一个接口
  2. 在 Java 中将接口的实现类传递给 C/C++
  3. 在 C/C++ 中保存这个对象的引用
  4. 在 C/C++ 中通过 Call<Type>Method 调用接口的方法

注意:为了避免 Java 对象被 GC 回收,你需要使用 NewGlobalRef 来创建一个全局引用,使用完毕后,必须用 DeleteGlobalRef 释放它,否则会造成内存泄漏。

NDK 与 ABI (Application Binary Interface)

不同的 CPU 架构(如 ARM, x86)需要不同的编译代码,ABI 定义了二进制接口。

  • armeabi-v7a: 32 位 ARM 架构(最广泛支持)
  • arm64-v8a: 64 位 ARM 架构
  • x86: 32 位 Intel 架构
  • x86_64: 64 位 Intel 架构

app/build.gradle 中,你可以指定要支持的 ABI:

android {
    defaultConfig {
        ...
        ndk {
            // 指定支持的 ABI
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
}

CMake 会根据这个配置为指定的 ABI 分别编译 .so 文件,并将它们打包到 APK 的 lib/ 目录下。


常见问题与最佳实践

  • 内存泄漏

    • 全局引用:使用 NewGlobalRef 后,一定要记得用 DeleteGlobalRef 释放。
    • 局部引用:在 C/C++ 函数中创建的局部引用(如通过 FindClass, NewObject 创建的)会在函数返回时自动释放,但如果函数很长,或者需要跨 JNI 调用传递,应该用 NewGlobalRef 转换为全局引用。
    • 全局缓存:避免缓存 jclassjmethodID 在一个函数中频繁调用的地方,除非你确定它们不会改变,如果缓存,必须用 NewGlobalRef
  • 性能

    • JNI 调用本身是有开销的,尽量减少 Java 和 Native 之间的切换次数。
    • 避免在循环中进行频繁的 JNI 调用,尽量将数据批量传递给 Native 代码处理。
  • 线程

    • 默认情况下,一个线程只能使用它自己附加的 JNIEnv,你不能在一个线程中创建 JNIEnv,然后在另一个线程中使用它。
    • 如果子线程需要调用 JNI,必须先通过 AttachCurrentThread 将自己附加到 JVM,获取 JNIEnv,并在使用完毕后通过 DetachCurrentThread 分离。

总结与学习资源

JNI 是一个强大的工具,但也是一把双刃剑,滥用它会带来难以调试的崩溃和内存问题,请遵循以下原则:

  1. 保持接口简单:尽量让 native 方法只做少量、核心的工作。
  2. 数据传递高效:尽量传递大块数据(如 byte[]),而不是频繁地传递小块数据。
  3. 管理好内存:始终记住 JNI 引用的生命周期,及时释放全局引用。
  4. 善用日志:在 C/C++ 中使用 __android_log_print 来调试你的 native 代码。

学习资源

祝你学习顺利!

分享:
扫描分享到社交APP
上一篇
下一篇