Android JNI 教程:从入门到实践
目录
- 什么是 JNI?为什么需要它?
- 环境准备
- JNI 基础知识详解
- JNI 函数命名规则
- 基本数据类型与引用类型
- 字符串处理 (
jstring) - 数组处理 (
jarray) - 方法调用 (
Call<Type>Method) - 异常处理
- 实战演练:一个完整的 JNI 项目
- 第一步:创建 Android 项目并配置 CMake
- 第二步:在 Java/Kotlin 中声明
native方法 - 第三步:使用
javah或javac -h生成 C/C++ 头文件 - 第四步:实现 C/C++ 代码
- 第五步:配置 CMakeLists.txt 以编译 C/C++ 代码
- 第六步:加载并调用 native 方法
- 进阶主题
- 在 C/C++ 中创建和操作 Java 对象
- 处理回调
- NDK 与 ABI (Application Binary Interface)
- 常见问题与最佳实践
- 总结与学习资源
什么是 JNI?为什么需要它?
JNI (Java Native Interface),即 Java 本地接口,是 Java 平台的一部分,它允许 Java 代码和其他语言(主要是 C、C++ 和汇编)编写的代码进行交互。

为什么需要 JNI?
- 性能优化:对于计算密集型任务(如图像处理、物理模拟、游戏引擎),用 C/C++ 实现会比纯 Java/Kotlin 快得多。
- 复用现有库:项目中可能已经存在成熟、高效的 C/C++ 库(如 OpenCV、FFmpeg),通过 JNI 可以在 Android 中直接调用它们,而无需重新开发。
- 访问底层硬件:某些硬件功能可能没有提供 Java API,但可以通过 C/C++ 直接访问硬件驱动。
- 平台特定代码:当需要实现平台特有的功能时,可以使用 JNI 来编写平台相关的代码。
核心思想:JNI 是一座桥梁,一头连着 Java 虚拟机,另一头连着本地(Native)代码。
环境准备
在开始之前,请确保你的开发环境已经配置好:
- 安装 Android Studio:最新版本即可。
- 配置 NDK:
- 打开 Android Studio,进入
Tools->SDK Manager。 - 在
SDK Tools标签页中,勾选NDK (Side by side)和CMake。 - 点击
Apply进行安装。
- 打开 Android Studio,进入
- 创建或打开一个支持 C++ 的项目:
- 创建新项目时,选择
Native C++模板,它会自动帮你配置好 CMake 和基本结构。 - 如果已有项目,可以通过
Tools->SDK Manager->NDK (Side by side)下载后,手动配置。
- 创建新项目时,选择
JNI 基础知识详解
JNI 函数命名规则
当你从 C/C++ 调用一个 Java 方法时,JVM 需要找到这个方法的实现,JNI 使用一套严格的命名规则来定位方法,这个函数名由以下几部分组成:

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 指针。
- 获取类对象:
jclass clazz = env->GetObjectClass(obj); - 获取方法ID:
jmethodID mid = env->GetMethodID(clazz, "methodName", "(Ljava/lang/String;)V");(签名是参数类型)返回值类型) - 调用方法:
env->CallVoidMethod(obj, mid, j_str);
Call<Type>Method 系列函数:
CallVoidMethod: 返回值为 voidCallIntMethod: 返回值为 intCallObjectMethod: 返回值为 ObjectCallStatic<Type>Method: 调用静态方法
异常处理
JNI 调用可能会抛出 Java 异常,C/C++ 代码必须检查并处理这些异常,否则可能会导致程序崩溃。
// 在可能抛出异常的 JNI 调用后检查
if (env->ExceptionCheck()) {
// 打印异常信息到 Logcat
env->ExceptionDescribe();
// 清除异常,否则后续 JNI 调用会失败
env->ExceptionClear();
// ... 进行错误处理
}
实战演练:一个完整的 JNI 项目
我们将创建一个简单的例子,让 C++ 代码返回一个字符串给 Java/Kotlin。
第一步:创建 Android 项目并配置 CMake
- 新建一个 Android 项目,选择
Native C++模板。 - 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 对象。
- 找到构造函数的 ID:
GetMethodID(clazz, "<init>", "()V");(构造函数名永远是<init>) - 创建对象:
jobject obj = env->NewObject(clazz, constructor_id); - 访问对象的字段:使用
GetFieldID和SetFieldID。
处理回调
这是一个非常常见的场景:Java/Kotlin 设置一个回调,C/C++ 在某个事件发生时调用它。
- 在 Java 中定义一个接口。
- 在 Java 中将接口的实现类传递给 C/C++。
- 在 C/C++ 中保存这个对象的引用。
- 在 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转换为全局引用。 - 全局缓存:避免缓存
jclass或jmethodID在一个函数中频繁调用的地方,除非你确定它们不会改变,如果缓存,必须用NewGlobalRef。
- 全局引用:使用
-
性能:
- JNI 调用本身是有开销的,尽量减少 Java 和 Native 之间的切换次数。
- 避免在循环中进行频繁的 JNI 调用,尽量将数据批量传递给 Native 代码处理。
-
线程:
- 默认情况下,一个线程只能使用它自己附加的
JNIEnv,你不能在一个线程中创建JNIEnv,然后在另一个线程中使用它。 - 如果子线程需要调用 JNI,必须先通过
AttachCurrentThread将自己附加到 JVM,获取JNIEnv,并在使用完毕后通过DetachCurrentThread分离。
- 默认情况下,一个线程只能使用它自己附加的
总结与学习资源
JNI 是一个强大的工具,但也是一把双刃剑,滥用它会带来难以调试的崩溃和内存问题,请遵循以下原则:
- 保持接口简单:尽量让 native 方法只做少量、核心的工作。
- 数据传递高效:尽量传递大块数据(如
byte[]),而不是频繁地传递小块数据。 - 管理好内存:始终记住 JNI 引用的生命周期,及时释放全局引用。
- 善用日志:在 C/C++ 中使用
__android_log_print来调试你的 native 代码。
学习资源:
- 官方文档:Android NDK Guide - 最权威的资料。
- Oracle JNI 规范:Java Native Interface Specification - 理解底层原理。
- GitHub 示例:Google 提供了许多优秀的 NDK 示例,如 android-ndk-samples。
- Stack Overflow:遇到具体问题时,这里是最好的求助地。
祝你学习顺利!
