杰瑞科技汇

Android C如何调用Java方法?

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

Android C如何调用Java方法?-图1
(图片来源网络,侵删)

下面我将为你详细解释整个流程,从环境搭建到具体的代码实现,并提供一个完整的示例。


核心概念

  1. JNI (Java Native Interface): 是 Java 平台的一部分,它是一套编程接口,允许运行在 Java 虚拟机 中的代码与其他语言(如 C/C++)编写的代码进行交互。
  2. JNI 函数指针: 在 C/C++ 代码中,你不能直接调用 Java 方法,你需要通过一个特殊的指针 JNIEnv* 来获取 JNI 提供的一系列函数,然后通过这些函数来间接操作 Java 对象和方法。
  3. JVM (Java Virtual Machine): JNI 是通过一个指向 JVM 的指针 JavaVM* 来访问的。JavaVM 是 JVM 在 C/C++ 层的表示,可以用来附加或分离当前线程。

完整调用流程

我们将通过一个完整的例子来演示 C 如何调用 Java,这个例子将实现:

  1. Java 层: 定义一个 NativeUtils 类,其中包含一个 native 方法 callJavaMethod()
  2. C 层: 实现 callJavaMethod(),在该函数中,它会:
    • 获取 Java 的 String 类。
    • 创建一个 Java 字符串对象 ("Hello from C!")。
    • 获取 Java 层 NativeUtils 类的一个实例。
    • 调用该实例的 printString(String message) 方法,并将 C 层创建的字符串对象传过去。

第 1 步:准备 Java 代码

创建一个 Java 类,其中声明一个 native 方法。

app/src/main/java/com/example/jnidemo/NativeUtils.java

Android C如何调用Java方法?-图2
(图片来源网络,侵删)
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 自带的 javacjavah (或新版本 JDK 中的 javac -h) 工具来生成一个 C 头文件,这个头文件包含了 Java 方法的 C 语言签名。

  1. 编译 Java 文件:

    # 确保你已经编译了 Java 文件
    # 在 Android Studio 中,Build -> Make Project 即可
  2. 生成头文件:

    Android C如何调用Java方法?-图3
    (图片来源网络,侵删)
    • 对于旧版 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

执行后,你会在 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>

运行和查看结果

  1. 运行你的 App。
  2. 点击按钮。
  3. 打开 Android Studio 的 Logcat 窗口,筛选标签为 JNIDemo

你将看到如下日志输出:

I/JNIDemo: C 代码: 开始执行 callJavaMethod
I/JNIDemo: C 代码: 调用 Java 方法完成
I/JNIDemo: Java 收到来自 C 的消息: Hello from C!

这证明了 C 代码成功调用了 Java 代码。

总结与最佳实践

  1. 线程: 默认情况下,只有创建 JVM 的主线程可以调用 JNI 函数,如果你在后台线程(如通过 new Thread() 创建的线程)中调用 JNI,你需要先通过 AttachCurrentThread 将该线程附加到 JVM。
  2. 异常: JNI 调用 Java 方法时可能会抛出异常,C/C++ 代码中需要检查是否有异常发生,并用 ExceptionCheckExceptionDescribe 处理,否则可能导致程序崩溃。
  3. 引用类型:
    • 局部引用: 由 JNI 函数创建,只在当前线程的当前 JNI 调用中有效,用完后必须手动 DeleteLocalRef,否则会内存泄漏。
    • 全局引用: 通过 NewGlobalRef 从局部引用创建,可以跨线程和跨 JNI 调用使用,直到你手动 DeleteGlobalRef,它需要显式释放。
    • 弱全局引用: 功能类似全局引用,但可以被垃圾回收器回收,适用于缓存场景。
  4. 性能: JNI 调用本身有开销,应尽量减少 Java 和 C/C++ 之间的切换,避免在循环中进行频繁的 JNI 调用,尽量在 C/C++ 层完成复杂计算,只将最终结果返回给 Java 层。

这个流程涵盖了 C 调用 Java 的所有核心步骤,掌握了它,你就可以在 Android 中灵活地运用 C/C++ 来提升性能和复用现有代码了。

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