杰瑞科技汇

Android Java如何调用C代码?

Android Java调用C/C++完全指南:从JNI到NDK,一篇搞定!

** 还在为Android性能瓶颈发愁?深入浅出解析Java与C/C++高效通信,解锁原生代码强大威力!

描述(Description): 本文详细讲解Android开发中Java如何调用C/C++代码,涵盖JNI基础、NDK开发流程、数据类型映射、方法签名及实战案例,无论是性能优化还是复用现有库,这篇指南都将助你快速掌握Android Java调用C的核心技术,解决开发难题,提升应用性能。


引言:为什么Android开发者需要学会Java调用C/C++?

在Android开发的日常中,我们主要使用Java(或Kotlin)语言进行快速开发,你是否遇到过以下场景:

  1. 性能瓶颈: 某些计算密集型任务(如图像处理、物理模拟、音频编解码)用Java实现效率低下,无法满足流畅的用户体验。
  2. 复用现有库: 有大量成熟、高效的C/C++库(如OpenCV、FFmpeg)可以直接使用,无需重复造轮子。
  3. 访问硬件/系统API: 底层硬件操作或一些不对外开放的系统级API,通常只能通过C/C++来访问。

在这些情况下,Android Java调用C/C++ 就成为了一项至关重要的技能,它就像一座桥梁,连接了高级的Java世界和高效的C/C++世界,本文将带你从零开始,系统性地掌握这项技术,主要涉及两个核心概念:JNI(Java Native Interface)NDK(Native Development Kit)


第一部分:核心概念——JNI与NDK,它们是什么?

在深入代码之前,我们必须先理解两个主角:JNINDK

  • JNI (Java Native Interface): 它是一套编程接口,是Java虚拟机(JVM)与本地代码(如C/C++代码)进行交互的“官方协议”,你可以把它想象成Java和C/C++之间的“翻译官”和“外交官”,负责两者之间的数据传递和方法调用。JNI是“怎么做”的规范。

  • NDK (Native Development Kit): 它是一套开发工具包,Google为Android开发者提供的,NDK包含了用于编译、调试本地代码的工具链(如GCC/Clang)、头文件(包含JNI定义)、库文件以及构建脚本(如CMake)。NDK是“用什么做”的工具集。

我们使用NDK提供的工具和环境,来编写符合JNI规范的C/C++代码,从而实现Java对C/C++的调用。


第二部分:实战演练——Java如何调用C/C++?

理论说再多不如动手实践,我们将通过一个经典的“Hello, World!”示例,走一遍完整的流程。

步骤1:准备工作——配置NDK环境

确保你的Android Studio已经安装并配置好NDK,路径:Tools -> SDK Manager -> SDK Tools,勾选 NDK (Side by side)CMake,然后点击Apply安装。

步骤2:创建支持C++的Android项目

  1. 新建一个Android项目。
  2. 在向导的 "Add an activity to Mobile" 步骤,选择 "Native C++"。
  3. 填写项目信息,点击Finish。

这样,Android Studio会自动为你生成一个包含C++支持的项目结构,并预置了一个示例。

步骤3:编写Java代码,声明Native方法

打开 MainActivity.java,我们将在这里声明一个要被C/C++实现的方法。

package com.example.jnitest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import java.lang.String; // 导入String类,用于JNI类型映射
public class MainActivity extends AppCompatActivity {
    // 1. 声明一个native方法
    // 注意:这个方法没有方法体,它的实现将在C/C++中完成
    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);
        // 2. 调用native方法
        tv.setText(stringFromJNI());
    }
}

关键点:

  • 使用 native 关键字修饰方法,告诉编译器这个方法的实现在本地(Native)代码中。
  • 需要手动加载包含本地代码的库,我们将在下一步完成。

步骤4:加载本地库并生成JNI头文件

  1. 加载库:MainActivity.javaonCreate 方法中,或任何合适的静态代码块中,添加加载库的代码。

    static {
        // 加载我们即将创建的本地库
        // 库的命名规则是 lib<库名>.so
        // 这里库名是 "jnitest",那么库文件就是 "libjnitest.so"
        System.loadLibrary("jnitest");
    }
  2. 生成JNI头文件:

    • 首先Build一下项目,确保 MainActivity.class 文件生成。
    • 打开Android Studio的Terminal(终端),切换到你的 app/src/main/java 目录。
    • 执行以下命令(请根据你的JDK路径和包名调整):
    # javah 是一个工具,用于从.class文件生成JNI头文件
    # 在较新版本的JDK中,javah已被javac的 -h 选项取代
    # 推荐使用新方式:
    javac -cp $(pwd):$(dirname $(readlink -f $(which javac)))/../lib/tools.jar -d . -h ../cpp com/example/jnitest/MainActivity.java
    • 更简单的方法(Android Studio内置):
      • 右键点击 MainActivity.java -> External Tools -> javah
      • 如果没有,可以在 Settings/Preferences -> Tools -> External Tools 中新建一个,配置 javah 命令。

    执行成功后,你的 app/src/main/cpp 目录下会生成一个名为 com_example_jnitest_MainActivity.h 的头文件。

步骤5:编写C/C++实现

打开 app/src/main/cpp 目录下的 native-lib.cpp 文件(或者你新创建的cpp文件),并实现头文件中声明的函数。

#include <jni.h>
#include <string>
// #include "com_example_jnitest_MainActivity.h" // 如果头文件不在标准位置,需要手动包含
// 实现头文件中声明的函数
// 函数名格式:Java_包名_类名_方法名
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_jnitest_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    // env: JNI接口指针,用于操作Java对象
    // this: 如果是静态方法,这个参数是NULL;如果是实例方法,是调用该方法的Java对象引用
    // 创建一个C++字符串
    std::string hello = "Hello from C++!";
    // 将C++字符串转换为Java字符串并返回
    // env->NewStringUTF 是JNI提供的函数
    return env->NewStringUTF(hello.c_str());
}

关键点:

  • 函数名: 函数名必须严格按照 Java_包名_类名_方法名 的格式,下划线 _ 代替 ,这是JNI的“签名”。
  • 参数:
    • JNIEnv* env:最重要的参数,是JNI环境的指针,所有JNI函数都通过它来调用。
    • jobjectjclass:代表调用该方法的Java对象(实例方法)或类对象(静态方法)。
  • extern "C" 这是C++的关键字,它告诉编译器使用C语言的链接方式,避免C++的名称修饰(Name Mangling),确保函数名能被正确找到。

步骤6:配置CMake并构建

打开 app/build.gradle 文件,确保 externalNativeBuild 配置正确。

android {
    // ... 其他配置
    defaultConfig {
        // ... 其他配置
        externalNativeBuild {
            cmake {
                cppFlags ""
                // 可以指定ABI,例如只支持 arm64-v8a
                // abiFilters 'arm64-v8a'
            }
        }
    }
    // ... 其他配置
    // 配置CMake路径
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.18.1" // 使用你安装的CMake版本
        }
    }
}

打开 app/src/main/cpp/CMakeLists.txt,添加你的源文件。

# 指定CMake最低版本
cmake_minimum_required(VERSION 3.4.1)
# 定义一个名为 native-lib 的库
add_library(
        # 库名
        native-lib
        # SHARED 表示生成一个共享库 (.so文件)
        SHARED
        # 源文件
        native-lib.cpp)
# 找到 log 库
find_library(
        log-lib
        log)
# 将 log 库链接到 native-lib
target_link_libraries(
        # 目标库
        native-lib
        # 链接的库
        ${log-lib})

点击Android Studio的 "Build" -> "Make Project" 或 "Rebuild Project",编译成功后,你的 app/build/intermediates/cmake/debug/obj 目录下会生成 libjnitest.so 文件。

运行你的App,如果一切顺利,你将在屏幕上看到 "Hello from C++!"。


第三部分:深入理解——数据类型与方法签名

JNI数据类型映射

Java和C/C++有各自的数据类型,JNI定义了一套中间类型来对应它们。

Java 类型 JNI 类型 C/C++ 类型 描述
boolean jboolean unsigned char 8位,true/false
byte jbyte signed char 8位
char jchar unsigned short 16位,Unicode字符
short jshort short 16位
int jint int 32位
long jlong long long 64位
float jfloat float 32位
double jdouble double 64位
void void void 无返回值
java.lang.Object jobject jobject 所有Java对象的基类
java.lang.Class jclass jclass Java类对象
java.lang.String jstring jstring Java字符串
Object[] jobjectArray jobjectArray Java对象数组
boolean[] jbooleanArray jbooleanArray Java布尔数组
byte[] jbyteArray jbyteArray Java字节数组
...

核心操作:

  • 创建Java对象: env->NewStringUTF("..."), env->NewObject(...)
  • 获取Java对象字段: env->GetObjectField(...), env->GetIntField(...)
  • 设置Java对象字段: env->SetObjectField(...), env->SetIntField(...)
  • 调用Java方法: env->CallVoidMethod(...), env->CallIntMethod(...)
  • 基本类型数组操作: env->GetByteArrayElements(), env->ReleaseByteArrayElements() (注意释放资源!)

方法签名

JNI中,方法参数和返回值需要一种紧凑的方式来表示,这就是方法签名

  • 基本类型签名:

    • Z -> boolean
    • B -> byte
    • C -> char
    • S -> short
    • I -> int
    • J -> long
    • F -> float
    • D -> double
    • V -> void
  • 对象类型签名:

    • Ljava/lang/String; -> String (L + 全限定名 + ;)
    • Ljava/lang/Object; -> Object
    • [I -> int[] ([ + 类型签名)
    • [[Ljava/lang/String; -> String[][] (多个[表示多维)
  • 方法签名格式: 参数签名返回值签名

示例: public int add(int a, int b) 的签名为 (II)I public String getName() 的签名为 ()Ljava/lang/String; public void process(byte[] data) 的签名为 ([B)V

你可以使用 javap 工具来查看类的签名: javap -s -p com.example.jnitest.MainActivity


第四部分:进阶技巧与最佳实践

  1. 性能优化与线程:

    • 避免频繁调用: JNI调用本身有开销,不要将简单的逻辑也放到C/C++中。
    • 数据拷贝: 传递大数组(如Bitmap)时,尽量使用 GetPrimitiveArrayCriticalReleasePrimitiveArrayCritical 来减少拷贝,但要注意这会阻塞GC,时间要尽可能短。
    • 线程: C/C++代码运行在独立的Native线程中,与Java线程不是同一个,如果要在C/C++中操作Java对象,必须确保该Java对象所在的线程是可附加的(Attachable),并且要正确处理线程同步问题。
  2. 内存管理:

    • 谁分配,谁释放: 在JNI中创建的局部引用(jobject, jstring等),在函数返回时会自动释放,但如果你在循环中大量创建,可能会造成内存泄漏,可以使用 env->DeleteLocalRef() 手动释放。
    • 全局引用: 如果需要在多个函数或长时间内使用一个Java对象,必须将其转换为全局引用 env->NewGlobalRef(),并在使用完毕后手动 env->DeleteGlobalRef() 释放。
    • 全局缓存: 对于频繁使用的 jclass 或 jmethodID,可以在类加载时缓存起来,避免每次调用都去查找。
  3. 错误处理:

    • 检查所有JNI函数的返回值,特别是可能返回NULL的函数(如 NewStringUTF, GetObjectClass 等)。
    • 使用 ExceptionCheck()ExceptionDescribe() 来处理JNI调用中可能发生的Java异常。

第五部分:总结与展望

通过本文,我们系统地学习了Android Java调用C/C++的完整流程,从JNI/NDK的基本概念,到实战编码,再到数据类型、方法签名和最佳实践。

回顾核心要点:

  • JNI是接口,NDK是工具。
  • Java声明native方法,C/C++按Java_包名_类名_方法名规则实现。
  • *`JNIEnv`是JNI操作的入口,负责所有与Java世界的交互。**
  • 注意数据类型映射和内存管理,避免内存泄漏和崩溃。

掌握这项技术,意味着你的Android开发能力不再局限于Java/Kotlin,能够自如地调用底层资源,优化性能,复用海量C/C++生态,成为一名更全面的开发者。

随着Flutter等跨平台框架的兴起,虽然它们也在底层使用C++,但对于需要极致性能和深度硬件交互的应用来说,直接使用JNI/NDK依然是不可替代的“终极武器”,希望这篇文章能成为你探索Android原生开发新高度的有力跳板!


SEO关键词与标签(Tags)

  • 核心关键词: android java调用c, android jni, android ndk, java调用c++, android原生开发
  • 长尾关键词: android studio jni配置, android c++数据类型, android ndk开发教程, android native方法, android 性能优化, android调用so库
  • #Android开发 #JNI #NDK #Java #C++ #性能优化 #移动开发 #技术教程 #编程
分享:
扫描分享到社交APP
上一篇
下一篇