杰瑞科技汇

Android Java如何调用C代码?

核心概念:JNI (Java Native Interface)

JNI 是 Java 平台的一部分,它定义了一套规范,允许 Java 代码和其他语言(如 C, C++, Assembly)进行交互,它是一座桥梁,连接着 Java 世界和原生代码世界。

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

为什么需要 JNI?

  • 性能优化:对于图像处理、物理模拟、音视频编解码等计算密集型任务,C/C++ 的执行效率远高于 Java。
  • 硬件交互:直接操作硬件(如 GPS、传感器、蓝牙)通常需要通过 C/C++ 驱动。
  • 代码复用:利用现有的、成熟的 C/C++ 库(如 OpenSSL、FFmpeg)。
  • 保护核心算法:将关键逻辑放在 C/C++ 中,比纯 Java 更难被逆向工程。

调用流程概览

整个过程可以分为以下几个步骤:

  1. 编写 Java 代码:在 Java 类中声明 native 方法。
  2. 生成 C/C++ 头文件:使用 javacjavah(或新版 Android Studio 的自动工具)生成包含 C 函数原型的头文件。
  3. 实现 C/C++ 代码:创建 .c.cpp 文件,实现头文件中声明的函数。
  4. 构建原生库:将 C/C++ 代码编译成 .so (Shared Object) 文件。
  5. 打包并调用:将 .so 文件打包到 APK 的正确目录下,并在 Java 代码中加载并调用。

详细步骤与代码示例

我们将创建一个简单的示例:Java 调用一个 C 函数,该函数接收两个整数并返回它们的和。

步骤 1: 创建 Android 项目并编写 Java 代码

  1. 在 Android Studio 中创建一个新的项目。
  2. 创建一个 Java 类,com.example.jnidemo.NativeUtils
// NativeUtils.java
package com.example.jnidemo;
public class NativeUtils {
    // 1. 声明一个 native 方法
    // 注意:方法名需要遵循特定的命名规则:Java_包名_类名_方法名
    public native int add(int a, int b);
    // 2. 加载原生库
    // System.loadLibrary 会在库的路径下寻找 "lib库名.so"
    // 所以这里的库名是 "jnidemo",对应的库文件是 "libjnidemo.so"
    static {
        System.loadLibrary("jnidemo");
    }
}

步骤 2: 生成 C/C++ 头文件 (自动生成)

在较新版本的 Android Studio 中,这个过程是自动化的。

Android Java如何调用C代码?-图2
(图片来源网络,侵删)
  1. 将光标放在 add(int a, int b) 方法上。
  2. Alt + Enter (或 Option + Enter)。
  3. 在弹出的菜单中选择 "Create function...""Create implementation..."
  4. 在接下来的对话框中,选择 "C/C++ Header"
  5. Android Studio 会在 app/src/main/cpp/ 目录下自动创建一个 NativeUtils.h 文件。

生成的 NativeUtils.h 文件内容如下:

// NativeUtils.h
/* 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:    add
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_example_jnidemo_NativeUtils_add
  (JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif

关键点解析:

  • #include <jni.h>:包含了 JNI 的所有核心定义和数据类型。
  • JNIEXPORTJNICALL:是 JNI 的关键字,用于指定函数的调用规范。
  • jint:JNI 对应 Java int 类型的数据类型。
  • JNIEnv *:一个指向 JNI 环境的指针,通过这个指针,你可以在 C 代码中调用 Java 的方法、操作 Java 对象等。
  • jobject:代表调用这个 native 方法的 Java 对象实例(如果是静态方法,则为 jobject 代表 Class 对象)。
  • Java_com_example_jnidemo_NativeUtils_add:这就是 Java 和 C 函数的绑定名称,规则是 Java_完整类名_方法名

步骤 3: 实现 C/C++ 代码

app/src/main/cpp/ 目录下创建一个 native-lib.cpp 文件(如果不存在),并实现 add 函数。

// native-lib.cpp
#include <jni.h>
#include <string>
// 实现 Java_com_example_jnidemo_NativeUtils_add 函数
extern "C" JNIEXPORT jint JNICALL
Java_com_example_jnidemo_NativeUtils_add(
        JNIEnv* env,
        jobject /* this */,
        jint a,
        jint b) {
    // 直接返回两个数的和
    return a + b;
}

步骤 4: 配置 CMake 构建脚本

这是将 C/C++ 代码编译成 .so 文件的关键配置。

Android Java如何调用C代码?-图3
(图片来源网络,侵删)
  1. 打开 app/build.gradle 文件,确保 externalNativeBuild 已配置,并指向 CMakeLists.txt
// app/build.gradle
android {
    // ...
    defaultConfig {
        // ...
        externalNativeBuild {
            cmake {
                cppFlags ""
                // 可以指定支持的 ABI 架构
                // abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
            }
        }
    }
    // ...
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.18.1" // 使用你安装的 CMake 版本
        }
    }
}
  1. 确保 app/src/main/cpp/CMakeLists.txt 文件存在并配置正确。
# CMakeLists.txt
# 设置 C++ 标准
cmake_minimum_required(VERSION 3.4.1)
# 定义库名,必须与 Java 代码中 System.loadLibrary("库名") 一致
add_library( # 设置库的名称
             jnidemo # 库名
             # 设置库的类型
             SHARED # 共享库,即 .so 文件
             # 提供源文件的相对路径
             native-lib.cpp )
# 找到指定的本机平台库
find_library( # 设置路径变量的名称
              log-lib
              # 指定NDK库名称
              log )
# 将库链接到目标库
target_link_libraries( # 指定目标库
                       jnidemo
                       # 链接要包含的库
                       ${log-lib} )

步骤 5: 在 Java 中调用并测试

现在回到你的 Activity(MainActivity.java),加载库并调用 add 方法。

// MainActivity.java
package com.example.jnidemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 找到 TextView 用于显示结果
        TextView tv = findViewById(R.id.sample_text);
        // 创建 NativeUtils 对象并调用 native 方法
        NativeUtils nativeUtils = new NativeUtils();
        int result = nativeUtils.add(10, 20);
        // 在 TextView 中显示结果
        tv.setText("10 + 20 = " + result);
    }
}

运行 App,你将在屏幕上看到 "10 + 20 = 30"。


进阶:数据类型传递与回调

传递和返回字符串

Java 代码:

// NativeUtils.java
public native String sayHello();

C++ 代码:

// native-lib.cpp
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_jnidemo_NativeUtils_sayHello(
        JNIEnv* env,
        jobject /* this */) {
    // 使用 env->NewStringUTF 创建一个 Java 字符串
    return env->NewStringUTF("Hello from C++!");
}

传递 Java 对象并修改其字段

假设我们有一个 Person 类。

Person.java:

public class Person {
    public String name;
    public int age;
}

Java 代码:

// NativeUtils.java
public native void updatePerson(Person person);

C++ 代码:

// native-lib.cpp
#include <jni.h>
extern "C" JNIEXPORT void JNICALL
Java_com_example_jnidemo_NativeUtils_updatePerson(
        JNIEnv* env,
        jobject /* this */,
        jobject person) { // 接收一个 Java 对象
    // 1. 获取 Person 类的 class 对象
    jclass personClass = env->GetObjectClass(person);
    // 2. 获取字段 ID
    jfieldID nameFieldID = env->GetFieldID(personClass, "name", "Ljava/lang/String;");
    jfieldID ageFieldID = env->GetFieldID(personClass, "age", "I");
    // 3. 修改字段
    env->SetObjectField(person, nameFieldID, env->NewStringUTF("Alice"));
    env->SetIntField(person, ageFieldID, 30);
}

Java 中调用:

Person person = new Person();
person.name = "Bob";
person.age = 25;
nativeUtils.updatePerson(person);
// person.name 变为 "Alice", person.age 变为 30

重要注意事项与最佳实践

  1. 线程问题原生代码不是线程安全的! 你只能在创建该线程的 JNI 接口上调用 JNI 函数,从主线程调用的 native 方法,其 C 代码也只能在主线程执行,耗时操作必须在新线程中处理。
  2. 性能开销:Java 和 C 之间的数据传递(尤其是对象和数组)是有性能开销的,应尽量减少频繁的跨语言调用,一次性传递大量数据比多次传递少量数据更高效。
  3. 内存管理
    • C 分配的内存:如果在 C 中使用 malloc 分配了内存,必须用 free 释放,否则会导致内存泄漏。
    • JNI 引用JNIEnv 提供的 NewGlobalRef, NewLocalRef 等创建的 JNI 引用,如果不手动释放,会导致内存泄漏,全局引用需要 DeleteGlobalRef,局部引用在函数返回时自动释放,但在循环或长期运行的代码中需要手动 DeleteLocalRef
  4. 错误处理:C/C++ 代码中的错误(如空指针、除零)会导致 JVM 崩溃(SIGSEGV),必须在 C 代码中进行充分的检查和处理。
  5. ABIs (Application Binary Interfaces):不同的 CPU 架构(如 arm64-v8a, armeabi-v7a, x86_64)需要对应编译的 .so 文件,Android Studio 默认会为所有支持的 ABI 编译,APK 安装时,系统会自动选择合适的 .so 文件,为了减小 APK 大小,你可以通过 abiFilters 只打包你需要的架构。
  6. 现代替代方案
    • Kotlin/Native:Kotlin 官方提供的解决方案,可以编译成原生二进制文件,与 Java/Kotlin 交互比 JNI 更简单、更安全。
    • Runtime.exec():如果只是想调用一个外部的命令行工具,可以使用 Runtime.getRuntime().exec(),但这比 JNI 要重,且进程间通信更复杂。

希望这份详细的指南能帮助你掌握在 Android 中使用 Java 调用 C/C++ 的技能!

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