Android JNI 完整教程
目录
- 什么是 JNI?
- 为什么使用 JNI?
- 开发环境准备
- 创建第一个 JNI 项目
- 步骤 1: 创建 Android 项目
- 步骤 2: 编写 Java/Kotlin 代码并声明
native方法 - 步骤 3: 生成 JNI 头文件 (
*.h) - 步骤 4: 编写 C/C++ 实现代码 (
.cpp) - 步骤 5: 配置 CMake 构建脚本
- 步骤 6: 加载并调用 native 方法
- 步骤 7: 编译和运行
- JNI 核心数据类型与映射
- JNI 常用方法详解
- 获取基本类型字段
- 获取对象字段
- 调用实例方法
- 调用静态方法
- 异常处理
- JNI 高级主题
- 字符串处理
- 数组处理
- 全局引用与局部引用
- 线程管理
- 调试 JNI 代码
- 现代替代方案:Kotlin/Native 与 Jetpack Compose
- 总结与最佳实践
什么是 JNI?
JNI (Java Native Interface),即 Java 本地接口,是 Java 平台的一部分,它允许 Java 代码和其他语言(主要是 C、C++ 和汇编)编写的代码进行交互。

JNI 是一座桥梁,它让 Java 程序可以调用 C/C++ 编写的库,反之亦然,在 Android 开发中,我们主要用它来让 Java/Kotlin 代码调用 C/C++ 代码。
为什么使用 JNI?
在 Android 开发中,使用 JNI 并非首选,因为它增加了复杂性和维护成本,但在某些特定场景下,它是必要的:
- 性能优化:对于计算密集型任务(如图像处理、物理模拟、游戏引擎),C/C++ 的执行效率远高于 Java。
- 代码复用:利用现有的、成熟的 C/C++ 开源库(如 OpenCV、FFmpeg、SQLite),避免重复造轮子。
- 访问硬件/系统底层:直接操作硬件或调用 Android NDK 提供的、Java API 无法访问的系统级功能。
- 保护核心算法:将核心逻辑放在 C/C++ 中,比在 Java 中更难被反编译和窃取。
开发环境准备
在开始之前,请确保你的开发环境已经正确配置:
- Android Studio:推荐使用最新稳定版。
- NDK (Native Development Kit):Android SDK 的一部分,包含了用于编译 C/C++ 代码的工具链、库和头文件,在 Android Studio 的 SDK Manager 中安装。
- CMake:一个跨平台的自动化构建系统,用于编译 C/C++ 代码,同样在 SDK Manager 中安装。
- LLDB:用于调试 native 代码的调试器。
安装方法:打开 Android Studio -> Tools -> SDK Manager -> SDK Tools -> 勾选 NDK (Side by side) 和 CMake,然后点击 Apply 安装。

创建第一个 JNI 项目
我们将创建一个简单的项目,让 Java 调用一个 C++ 函数来计算两个整数的和。
步骤 1: 创建 Android 项目
- 打开 Android Studio,选择
File->New->New Project...。 - 选择
Native C++模板(这会自动帮我们配置好所有 JNI 环境),然后点击Next。 - 填写项目名称(如
JniDemo)、包名等,点击Next。 - 选择语言(Java 或 Kotlin)、最低 API 级别等,点击
Finish。
Android Studio 会自动为你创建一个包含 native 支持的项目结构。
步骤 2: 编写 Java/Kotlin 代码并声明 native 方法
打开 app/src/main/java/com/example/jnidemo/MainActivity.java (或 .kt)。
在 MainActivity 类中,声明一个 native 方法。native 关键字告诉 JVM 这个方法的实现在别处(即 C/C++ 中)。

package com.example.jnidemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// 声明一个 native 方法
// 注意:方法名必须遵循特定的规则:Java_全类名_方法名
// Java_com_example_jnidemo_MainActivity_stringFromJNI
// C++ 编译器会根据这个规则来查找对应的函数。
public native String stringFromJNI();
// 一个新的 native 方法,用于计算两数之和
public native int add(int a, int b);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
// 调用 native 方法
String hello = stringFromJNI();
tv.setText(hello);
// 调用我们自己的 native 方法
int sum = add(10, 20);
tv.append("\n10 + 20 = " + sum);
}
}
步骤 3: 生成 JNI 头文件 (*.h)
头文件定义了 C/C++ 函数的签名,告诉 C++ 编译器这个函数需要接收哪些参数。
- 在 Android Studio 的终端中,执行以下命令来编译 Java 代码并生成
.class文件:./gradlew assembleDebug
- 使用
javah工具(或现代的javac -h)从.class文件生成头文件,在较新的 Android Studio 和 JDK 版本中,推荐使用javac -h。# 假设你的 JDK 在 /usr/lib/jvm/java-11-openjdk-amd64/bin/ # 你需要找到 javac 的路径 /path/to/your/jdk/bin/javac -d . -h . app/build/intermediates/javac/debug/classes/com/example/jnidemo/MainActivity.class
-d .:将生成的.class文件输出到当前目录。-h .:将生成的 C/C++ 头文件输出到当前目录。
执行后,你会在项目根目录下找到一个名为 com_example_jnidemo_MainActivity.h 的头文件。
步骤 4: 编写 C/C++ 实现代码 (.cpp)
我们来实现头文件中声明的函数。
- 在 Android Studio 中,右键点击
app/src/main/cpp目录,选择New->C++ Source File,命名为native-lib.cpp。 - 将
native-lib.cpp的内容替换为:
#include <jni.h>
#include <string>
// 对应于 Java_com_example_jnidemo_MainActivity_stringFromJNI
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_jnidemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
// 将 std::string 转换为 Java 的 jstring
return env->NewStringUTF(hello.c_str());
}
// 对应于 Java_com_example_jnidemo_MainActivity_add
extern "C" JNIEXPORT jint JNICALL
Java_com_example_jnidemo_MainActivity_add(
JNIEnv* env,
jobject /* this */,
jint a,
jint b) {
return a + b;
}
代码解释:
extern "C":告诉 C++ 编译器使用 C 语言的链接方式,这非常重要,因为 Java 查找函数时使用的是 C 风格的函数名。JNIEXPORT,JNICALL:这是 JNI 规定的宏,用于声明函数的可见性和调用约定,必须加上。Java_com_example_jnidemo_MainActivity_stringFromJNI:这是函数名,必须与javac -h生成的规则完全一致。JNIEnv* env:指向 JNI 环境的指针,它是 JNI 的核心,通过它你可以调用所有 JNI 函数(如创建对象、访问字段等)。jobject this:native方法是实例方法,this就是调用该方法的对象实例,如果是静态方法,则传入jclass。jstring,jint:JNI 的数据类型,分别对应 Java 的String和int。env->NewStringUTF(...):通过JNIEnv指针调用 JNI 函数来创建一个 Java 字符串。
步骤 5: 配置 CMake 构建脚本
Android Studio 已经为我们生成了一个 CMakeLists.txt 文件,位于 app/src/main/cpp/ 目录下,我们需要确保它包含了我们的源文件。
打开 app/src/main/cpp/CMakeLists.txt应该类似这样:
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.18.1)
# 项目名称,可以自定义
project("jnidemo")
# 添加 C++ 标准
set(CMAKE_CXX_STANDARD 17)
# 查找 log 库,用于在 C/C++ 中打印日志
find_library(log-lib log)
# 添加你的 native 库
# add_library(库名称 SHARED 源文件列表)
add_library(
# Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp)
# 指定要链接的库
target_link_libraries(
# Specifies the target library.
native-lib
# Links the target library to the log library
# Includes the log library in the native library.
${log-lib})
这个脚本告诉 CMake 如何编译 native-lib.cpp 并生成一个名为 libnative-lib.so 的共享库。
步骤 6: 加载并调用 native 方法
在 MainActivity.java 中,我们还需要加载这个 .so 库,在 onCreate 方法中,添加 System.loadLibrary()。
public class MainActivity extends AppCompatActivity {
// ... native 方法声明 ...
static {
// 加载 native 库
// 库名是 "libnative-lib.so",这里只需要写 "native-lib"
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... 调用 native 方法 ...
}
}
System.loadLibrary() 会在应用的 lib/<abi> 目录下查找 lib库名.so 文件并加载它。
步骤 7: 编译和运行
点击 Android Studio 工具栏上的绿色 "Run" 按钮 (或按 Shift + F10),应用会安装到模拟器或真机上,并在屏幕上显示从 C++ 返回的字符串和计算结果。
JNI 核心数据类型与映射
理解 Java 和 C/C++ 数据类型的对应关系是使用 JNI 的关键。
| Java 类型 | JNI 类型 | C/C++ 类型 | 描述 |
|---|---|---|---|
byte |
jbyte |
signed char |
8位有符号整数 |
short |
jshort |
short |
16位有符号整数 |
int |
jint |
int |
32位有符号整数 |
long |
jlong |
long long |
64位有符号整数 |
float |
jfloat |
float |
32位浮点数 |
double |
jdouble |
double |
64位浮点数 |
char |
jchar |
unsigned short |
16位Unicode字符 |
boolean |
jboolean |
unsigned char |
8位布尔值 (true=1, false=0) |
Object |
jobject |
_jobject* |
任何 Java 对象的基类 |
Class |
jclass |
_jclass* |
Class 对象的引用 |
String |
jstring |
_jstring* |
String 对象的引用 |
Object[] |
jobjectArray |
_jobjectArray* |
对象数组 |
boolean[] |
jbooleanArray |
_jbooleanArray* |
布尔数组 |
byte[] |
jbyteArray |
_jbyteArray* |
字节数组 |
| ... | ... | ... | ... |
void |
void |
void |
无返回值 |
JNI 常用方法详解
JNIEnv 是一个结构体指针,它包含了大量的函数指针,这些函数就是 JNI API。JNIEnv 提供了丰富的操作。
获取基本类型字段
假设有一个 Java 类 Person:
public class Person {
public int age;
}
在 C++ 中获取 age 字段:
jfieldID ageFieldID = env->GetFieldID(personClass, "age", "I"); // (类, 字段名, 字段签名) jint age = env->GetIntField(personObject, ageFieldID);
GetFieldID: 获取字段ID,第三个参数是字段签名,I代表int,D代表double,Ljava/lang/String;代表String。GetIntField: 根据字段ID获取int值。
获取对象字段
获取 Person 对象的 name 字段:
public class Person {
public String name;
}
jfieldID nameFieldID = env->GetFieldID(personClass, "name", "Ljava/lang/String;"); jstring nameObj = (jstring)env->GetObjectField(personObject, nameFieldID); const char* nameStr = env->GetStringUTFChars(nameObj, nullptr); // ... 使用 nameStr ... env->ReleaseStringUTFChars(nameObj, nameStr); // 记得释放
调用实例方法
调用 Person 对象的 toString() 方法:
jmethodID toStringMethodID = env->GetMethodID(personClass, "toString", "()Ljava/lang/String;"); jstring resultStr = (jstring)env->CallObjectMethod(personObject, toStringMethodID);
GetMethodID: 获取方法ID,第三个参数是方法签名,()Ljava/lang/String;表示无参数,返回String。CallObjectMethod: 调用返回对象的方法,还有CallIntMethod,CallVoidMethod等。
调用静态方法
调用 Math 类的 max 方法:
jclass mathClass = env->FindClass("java/lang/Math");
jmethodID maxMethodID = env->GetStaticMethodID(mathClass, "max", "(II)I");
jint maxVal = env->CallStaticIntMethod(mathClass, maxMethodID, 10, 20);
FindClass: 根据类全名查找jclass。GetStaticMethodID: 获取静态方法ID。CallStaticIntMethod: 调用返回int的静态方法。
异常处理
JNI 调用可能会抛出 Java 异常,C/C++ 代码不会自动捕获这些异常,必须手动检查并处理。
// 在调用可能抛出异常的 JNI 函数后
env->CallVoidMethod(...); // 假设这个调用可能抛出异常
if (env->ExceptionCheck()) {
// 检查是否有异常发生
jthrowable exception = env->ExceptionOccurred();
env->ExceptionDescribe(); // 打印异常信息到 logcat
env->ExceptionClear(); // 清除异常,否则后续 JNI 调用会失败
// 你可以在这里决定如何处理异常,比如返回一个错误码
return -1;
}
JNI 高级主题
字符串处理
Java 使用 UTF-16 编码,而 C/C++ 常用 UTF-8 或平台编码。JNIEnv 提供了转换函数。
- Java -> C++:
jstring javaStr = ...; const char* cStr = env->GetStringUTFChars(javaStr, nullptr); // 转换为 UTF-8 // 使用 cStr env->ReleaseStringUTFChars(javaStr, cStr); // 必须释放!
- C++ -> Java:
const char* cStr = "Hello from C"; jstring javaStr = env->NewStringUTF(cStr); // 从 UTF-8 创建 Java 字符串
数组处理
JNI 提供了专门处理数组的方法,以避免频繁的内存拷贝。
-
获取数组元素指针:
jintArray intArray = ...; jboolean isCopy; jint* elements = env->GetIntArrayElements(intArray, &isCopy); // 可以直接操作 elements 指针,它指向数组或其副本 // 释放 if (isCopy == JNI_TRUE) { // 操作的是副本,需要将副本写回原数组 env->ReleaseIntArrayElements(intArray, elements, 0); } else { // 操作的是原数组,直接释放 env->ReleaseIntArrayElements(intArray, elements, JNI_ABORT); } -
创建新数组:
jintArray newArray = env->NewIntArray(10); // 创建一个长度为10的int数组
全局引用与局部引用
这是 JNI 中最常见也最容易出错的地方,与内存管理有关。
-
局部引用 (Local Reference):
- 生命周期:从创建它的 JNI 函数调用开始,到该函数返回结束。
- 作用域:仅限于当前 JNI 函数调用栈。
- 创建方式:绝大多数 JNI 函数(如
NewStringUTF,GetObjectClass,CallObjectMethod)返回的都是局部引用。 - 问题:如果在循环中创建大量局部引用,可能会导致 JNI 引用表溢出,引发崩溃。
- 手动释放:可以使用
env->DeleteLocalRef(ref)在函数返回前手动释放,尤其是在循环中。
-
全局引用 (Global Reference):
- 生命周期:从创建开始,直到你手动调用
DeleteGlobalRef释放它为止。 - 作用域:可以在任何线程、任何 JNI 函数中使用。
- 创建方式:通过
env->NewGlobalRef(localRef)将一个局部引用转换为全局引用。 - 用途:当你需要在 JNI 函数返回后仍然持有一个 Java 对象的引用时(在后台线程中使用)。
- 生命周期:从创建开始,直到你手动调用
最佳实践:
- 在大多数情况下,依赖局部引用的自动回收机制。
- 在循环中创建对象时,在循环结束后手动删除局部引用。
- 如果需要将一个 Java 对象的引用长期保存下来(在 C++ 类的成员变量中),务必将其转换为全局引用,并在不再需要时手动删除。
线程管理
-
主线程:在主线程(UI线程)中调用 native 方法,
JNIEnv是有效的。 -
子线程:在子线程中通过
JavaVM来获取当前线程的JNIEnv。// JavaVM 是一个全局对象,可以通过 JNI_OnLoad 获取或通过 AttachCurrentThread 获取 JavaVM* javaVM; // ... 在某个地方获取 javaVM ... JNIEnv* env; javaVM->AttachCurrentThread(&env, nullptr); // env 可以在这个子线程中使用了 // ... 调用 JNI 函数 ... javaVM->DetachCurrentThread(); // 线程结束时分离
-
JNI_OnLoad:当 native 库被加载时,会自动调用
JNI_OnLoad函数,你可以在这里做一些初始化工作,并保存JavaVM的指针。
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
javaVM = vm; // 保存 JavaVM 指针
return JNI_VERSION_1_6;
}
调试 JNI 代码
调试 JNI 代码分为两部分:Java 代码和 C/C++ 代码。
- 设置断点:像调试普通 Java 代码一样,在 Java 代码或 native 代码(
.cpp文件)中设置断点。 - 配置 Debuggable:确保
app/build.gradle中的android块里有debuggable true。android { ... buildTypes { release { ... } debug { debuggable true } } } - 启动调试:点击 "Debug" 按钮 (或
Shift + F9)。 - 切换线程:当应用停在 Java 断点时,调试器在 Java 线程,当应用停在 C/C++ 断点时,调试器会切换到本地线程,你可能需要在调试窗口的线程列表中手动切换回主线程来查看 Java 调用栈。
- 查看变量:在 C/C++ 断点处,你可以查看 C/C++ 变量,要查看 Java 对象的内容,可以使用
Android Logcat打印出来,或者在调试器的 "Evaluate Expression" 窗口中调用 JNI 函数(如env->GetObjectClass(obj))来检查。
现代替代方案:Kotlin/Native 与 Jetpack Compose
虽然 JNI 是传统且强大的方案,但 Google 也在推广更现代的解决方案:
- Kotlin/Native:Kotlin 团队开发的技术,可以直接将 Kotlin 代码编译成原生二进制文件(如
.so,.dylib,.dll),无需通过 JNI 桥接,它更安全,内存管理由编译器自动处理,但目前生态系统和工具链不如 NDK 成熟。 - Jetpack Compose + Skia:对于 UI 渲染,Google 推荐使用 Jetpack Compose,它底层使用 Skia 图形库进行高性能渲染,很多时候可以避免直接操作 OpenGL 或 Vulkan。
对于新项目,如果可能,优先考虑纯 Kotlin/Java 解决方案,如果必须使用 C/C++ 库,JNI 仍然是目前最主流和稳定的选择。
总结与最佳实践
- JNI 是最后的手段:只在性能、代码复用或访问底层等必要时使用。
- 保持接口简洁:尽量让 native 层的接口(函数)简单,传递基本类型和简单的数据结构,避免在 Java 和 C/C++ 之间频繁传递复杂对象。
- 管理内存和生命周期:深刻理解局部引用和全局引用的区别,避免内存泄漏和引用表溢出。
DeleteLocalRef和DeleteGlobalRef是你的好朋友。 - 错误处理:始终检查 JNI 调用是否抛出了 Java 异常。
- 线程安全:注意
JNIEnv的线程绑定特性,在子线程中正确使用JavaVM。 - 利用工具:熟练使用 Android Studio 的 CMake 配置、LLDB 调试器和 Logcat。
- 学习签名:熟悉 JNI 类型签名(如
I,D,Ljava/lang/String;),这是排查问题的关键。
这份教程涵盖了 JNI 开发的方方面面,希望能帮助你顺利地在 Android 项目中集成 C/C++ 代码。
