这个过程通常被称为 JNI (Java Native Interface, Java 本地接口)。

下面我将为你详细解释整个流程,从环境搭建到具体的代码实现,并提供一个完整的示例。
核心概念
- JNI (Java Native Interface): 是 Java 平台的一部分,它是一套编程接口,允许运行在 Java 虚拟机 中的代码与其他语言(如 C/C++)编写的代码进行交互。
- JNI 函数指针: 在 C/C++ 代码中,你不能直接调用 Java 方法,你需要通过一个特殊的指针
JNIEnv*来获取 JNI 提供的一系列函数,然后通过这些函数来间接操作 Java 对象和方法。 - JVM (Java Virtual Machine): JNI 是通过一个指向 JVM 的指针
JavaVM*来访问的。JavaVM是 JVM 在 C/C++ 层的表示,可以用来附加或分离当前线程。
完整调用流程
我们将通过一个完整的例子来演示 C 如何调用 Java,这个例子将实现:
- Java 层: 定义一个
NativeUtils类,其中包含一个 native 方法callJavaMethod()。 - C 层: 实现
callJavaMethod(),在该函数中,它会:- 获取 Java 的
String类。 - 创建一个 Java 字符串对象 ("Hello from C!")。
- 获取 Java 层
NativeUtils类的一个实例。 - 调用该实例的
printString(String message)方法,并将 C 层创建的字符串对象传过去。
- 获取 Java 的
第 1 步:准备 Java 代码
创建一个 Java 类,其中声明一个 native 方法。
app/src/main/java/com/example/jnidemo/NativeUtils.java

package com.example.jnidemo;
public class NativeUtils {
// 1. 声明一个 native 方法,这个方法由 C/C++ 实现
public native void callJavaMethod();
// 2. 一个普通的 Java 方法,供 C 代码调用
public void printString(String message) {
System.out.println("Java 收到来自 C 的消息: " + message);
}
// 3. 加载 C/C++ 动态库 (so 文件)
// 系统会自动在 lib 目录下寻找 libnative-lib.so
static {
System.loadLibrary("native-lib");
}
}
关键点:
native关键字告诉编译器,这个方法的实现不在 Java 中,而是在本地代码中。System.loadLibrary("native-lib")用于加载 C/C++ 编译生成的动态链接库(在 Android 上是.so文件)。native-lib是库名,编译后文件名为libnative-lib.so。
第 2 步:生成 JNI 头文件
这是连接 Java 和 C 的桥梁,你需要使用 JDK 自带的 javac 和 javah (或新版本 JDK 中的 javac -h) 工具来生成一个 C 头文件,这个头文件包含了 Java 方法的 C 语言签名。
-
编译 Java 文件:
# 确保你已经编译了 Java 文件 # 在 Android Studio 中,Build -> Make Project 即可
-
生成头文件:
(图片来源网络,侵删)- 对于旧版 JDK (使用
javah):# 假设你的 .class 文件在 build/intermediates/javac/debug/classes/com/example/jnidemo/ # 并且你的包名是 com.example.jnidemo javah -d jni -classpath build/intermediates/javac/debug/classes com.example.jnidemo.NativeUtils
- 对于新版 JDK (推荐,使用
javac -h):# -d 指定输出目录 (jni), -h 指定头文件输出目录 # -classpath 指定 .class 文件所在目录 javac -d jni -h jni -classpath build/intermediates/javac/debug/classes com/example/jnidemo/NativeUtils.java
- 对于旧版 JDK (使用
执行后,你会在 jni 目录下得到一个头文件,类似 com_example_jnidemo_NativeUtils.h。
jni/com_example_jnidemo_NativeUtils.h (这是一个示例,实际内容可能因 JDK 版本而异)
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jnidemo_NativeUtils */
#ifndef _Included_com_example_jnidemo_NativeUtils
#define _Included_com_example_jnidemo_NativeUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_jnidemo_NativeUtils
* Method: callJavaMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_jnidemo_NativeUtils_callJavaMethod
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
关键点:
- 函数名
Java_com_example_jnidemo_NativeUtils_callJavaMethod是固定的格式:Java_包名_类名_方法名。 JNIEnv *env: JNI 接口指针,是 JNI 函数的入口。jobject obj: 在 C/C++ 中,this引用被表示为jobject,如果是静态方法,则参数为jclass。
第 3 步:编写 C/C++ 实现代码
我们来实现 callJavaMethod 函数,并在其中调用 Java 的 printString 方法。
jni/native-lib.c
#include <jni.h>
#include <string.h>
#include <android/log.h> // 用于在 logcat 中打印日志
#define LOG_TAG "JNIDemo"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// 包含自动生成的头文件
#include "com_example_jnidemo_NativeUtils.h"
// 实现 Java_com_example_jnidemo_NativeUtils_callJavaMethod 函数
JNIEXPORT void JNICALL
Java_com_example_jnidemo_NativeUtils_callJavaMethod(JNIEnv *env, jobject thiz) {
LOGI("C 代码: 开始执行 callJavaMethod");
// 1. 获取 Java 的 String 类
jclass string_class = (*env)->FindClass(env, "java/lang/String");
if (string_class == NULL) {
LOGE("C 代码: 找不到 java/lang/String 类");
return;
}
// 2. 创建一个 Java 字符串对象 "Hello from C!"
jstring message_from_c = (*env)->NewStringUTF(env, "Hello from C!");
if (message_from_c == NULL) {
LOGE("C 代码: 创建字符串失败");
return;
}
// 3. 获取 NativeUtils 类的引用
// 因为 thiz 是 NativeUtils 的一个实例,所以我们可以用它来获取类
jclass native_utils_class = (*env)->GetObjectClass(env, thiz);
if (native_utils_class == NULL) {
LOGE("C 代码: 找不到 NativeUtils 类");
return;
}
// 4. 获取 printString 方法的 ID
// (Ljava/lang/String;)V 是方法的签名,表示接收一个 String 参数,返回 void
jmethodID print_method_id = (*env)->GetMethodID(env, native_utils_class, "printString", "(Ljava/lang/String;)V");
if (print_method_id == NULL) {
LOGE("C 代码: 找不到 printString 方法");
return;
}
// 5. 调用 Java 方法
// 参数: env, 对象实例, 方法ID, 方法参数
(*env)->CallVoidMethod(env, thiz, print_method_id, message_from_c);
// 6. 释放局部引用 (非常重要!)
// 局部引用会在当前 JNI 调用结束后自动释放,但为了性能和防止内存泄漏,最好手动释放
(*env)->DeleteLocalRef(env, string_class);
(*env)->DeleteLocalRef(env, message_from_c);
(*env)->DeleteLocalRef(env, native_utils_class);
LOGI("C 代码: 调用 Java 方法完成");
}
关键点:
- JNIEnv: 所有 JNI 函数的第一个参数。
- *`(env)->...JNIEnv` 是一个指向函数指针的结构体,所以需要通过解引用来调用其内部的函数。
FindClass: 根据类名(如java/lang/String)获取jclass。NewStringUTF: 根据 C 风格的字符串创建一个jstring对象。GetObjectClass: 从一个 Java 对象实例获取它的类。GetMethodID: 获取一个实例方法的 ID,需要提供方法名和签名。- 如何获取签名?可以使用
javap工具:javap -s -p build/intermediates/javac/debug/classes/com/example/jnidemo/NativeUtils.class
你会看到类似
printString(Ljava/lang/String;)V的输出,这就是签名。
- 如何获取签名?可以使用
CallVoidMethod: 调用返回类型为void的 Java 方法,还有CallIntMethod,CallObjectMethod等。DeleteLocalRef: 释放局部引用,对于在 JNI 函数中创建的大量对象,手动释放可以避免内存泄漏和性能问题。
第 4 步:配置 CMake 和构建
在 Android Studio 中,你需要配置 CMake 来编译你的 C/C++ 代码。
app/build.gradle
android {
// ...
defaultConfig {
// ...
externalNativeBuild {
cmake {
// 指定 C++ 标准
cppFlags ""
// 可以在这里指定 ABI,
// abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.18.1" // 使用你的 CMake 版本
}
}
}
app/src/main/cpp/CMakeLists.txt
# 设置 C++ 标准
cmake_minimum_required(VERSION 3.4.1)
# 定义你的 native-lib 库
add_library(
native-lib # 库名
SHARED # 共享库 (.so)
native-lib.c) # 你的 C/C++ 源文件
# 找到系统日志库
find_library(
log-lib
log)
# 链接库
# native-lib 依赖于 log-lib
target_link_libraries(
native-lib
${log-lib})
第 5 步:调用 native 方法
在你的 Activity 或其他地方调用 NativeUtils 的 native 方法。
app/src/main/java/com/example/jnidemo/MainActivity.java
package com.example.jnidemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button callButton = findViewById(R.id.call_button);
callButton.setText("调用 C 代码");
final NativeUtils nativeUtils = new NativeUtils();
callButton.setOnClickListener(v -> {
// 点击按钮时,调用 C 代码
nativeUtils.callJavaMethod();
});
}
}
app/src/main/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/call_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="调用 C 代码" />
</LinearLayout>
运行和查看结果
- 运行你的 App。
- 点击按钮。
- 打开 Android Studio 的
Logcat窗口,筛选标签为JNIDemo。
你将看到如下日志输出:
I/JNIDemo: C 代码: 开始执行 callJavaMethod
I/JNIDemo: C 代码: 调用 Java 方法完成
I/JNIDemo: Java 收到来自 C 的消息: Hello from C!
这证明了 C 代码成功调用了 Java 代码。
总结与最佳实践
- 线程: 默认情况下,只有创建 JVM 的主线程可以调用 JNI 函数,如果你在后台线程(如通过
new Thread()创建的线程)中调用 JNI,你需要先通过AttachCurrentThread将该线程附加到 JVM。 - 异常: JNI 调用 Java 方法时可能会抛出异常,C/C++ 代码中需要检查是否有异常发生,并用
ExceptionCheck和ExceptionDescribe处理,否则可能导致程序崩溃。 - 引用类型:
- 局部引用: 由 JNI 函数创建,只在当前线程的当前 JNI 调用中有效,用完后必须手动
DeleteLocalRef,否则会内存泄漏。 - 全局引用: 通过
NewGlobalRef从局部引用创建,可以跨线程和跨 JNI 调用使用,直到你手动DeleteGlobalRef,它需要显式释放。 - 弱全局引用: 功能类似全局引用,但可以被垃圾回收器回收,适用于缓存场景。
- 局部引用: 由 JNI 函数创建,只在当前线程的当前 JNI 调用中有效,用完后必须手动
- 性能: JNI 调用本身有开销,应尽量减少 Java 和 C/C++ 之间的切换,避免在循环中进行频繁的 JNI 调用,尽量在 C/C++ 层完成复杂计算,只将最终结果返回给 Java 层。
这个流程涵盖了 C 调用 Java 的所有核心步骤,掌握了它,你就可以在 Android 中灵活地运用 C/C++ 来提升性能和复用现有代码了。
