🎯 学习目标:通过本教程,你将掌握 Android JNI 开发的基础知识,学会如何在 Android 项目中集成和使用 C/C++ 代码。

📖 概述

JNI(Java Native Interface) 是 Java 平台的一部分,它允许在 Java 虚拟机内运行的 Java 代码调用并被用其他编程语言(如 C、C++)编写的应用程序和库调用。

将计算密集型任务移至 C/C++ 层执行,充分利用原生代码的性能优势:

  • 图像处理算法
  • 音视频编解码
  • 加密解密运算
  • 数学计算库

直接调用 Linux 系统底层 API,访问 Java 层无法直接使用的功能:

  • 串口通信
  • GPIO 控制
  • 文件系统操作
  • 网络底层协议

集成现有的成熟 C/C++ 库,避免重复开发:

  • OpenCV 图像处理
  • FFmpeg 音视频处理
  • OpenSSL 加密库
  • 第三方算法库

🛠️ 基本配置步骤

开始之前:确保你已经安装了 Android Studio 和相关开发工具

📋 前置要求

必需工具清单

  • Android Studio(最新版本)
  • NDK(Native Development Kit)
  • CMake 构建工具
  • Git(用于版本管理)

� 配置流程

开发提示:建议按照上述流程逐步配置,每完成一步都进行测试验证。

⚙️ 详细配置

1. Gradle 配置

app/build.gradle.kts 文件中添加 NDK 和 CMake 配置:

配置文件app/build.gradle.kts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
android {
// ...其他配置

defaultConfig {
// ...其他配置

// 配置 NDK 支持的 ABI 架构
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}

// 配置 CMake 构建系统
externalNativeBuild {
cmake {
path = file("CMakeLists.txt")
version = "3.18.1"
}
}
}

注意:ABI 架构建议根据目标设备选择,过多的架构会增加 APK 体积。

2. CMake 配置

在项目根目录创建 CMakeLists.txt 文件:

配置文件CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cmake_minimum_required(VERSION 3.18.1)

# 设置项目名称
project("SerialPortDemo")

# 添加库文件 - 创建名为 SerialPort 的共享动态库
add_library(
SerialPort # 库名称
SHARED # 库类型:共享库
src/main/cpp/SerialPort.h # 头文件
src/main/cpp/SerialPort.c # 源文件
)

# 链接 Android 系统库
target_link_libraries(
SerialPort # 目标库
android # Android 系统库
log # 日志库
)

构建说明:CMake 会自动处理跨平台编译,生成对应架构的 .so 动态库文件。

3. Java/Kotlin 中的 Native 方法声明

推荐使用:Kotlin 是 Android 官方首选语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.example.serialport

import java.io.FileDescriptor

class SerialPort private constructor() {

companion object {
// 静态初始化块 - 加载 Native 库
init {
System.loadLibrary("SerialPort")
}
}

/**
* 打开串口
* @param path 串口设备路径,如 "/dev/ttyS0"
* @param baudrate 波特率,如 9600, 115200
* @param flags 标志位
* @param parity 校验位:0-无校验,1-奇校验,2-偶校验
* @param stopbits 停止位:1 或 2
* @param databits 数据位:5, 6, 7, 8
* @return 文件描述符
*/
external fun open(
path: String,
baudrate: Int,
flags: Int,
parity: Int,
stopbits: Int,
databits: Int
): FileDescriptor?

/**
* 关闭串口
*/
external fun close()
}

经典选择:适合熟悉传统 Java 开发的开发者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.serialport;

import java.io.FileDescriptor;

public class SerialPort {

// 静态初始化块 - 加载 Native 库
static {
System.loadLibrary("SerialPort");
}

/**
* 打开串口(Native 方法)
*/
public native FileDescriptor open(String path, int baudrate, int flags,
int parity, int stopbits, int databits);

/**
* 关闭串口(Native 方法)
*/
public native void close();
}

4. C/C++ 实现

app/src/main/cpp/SerialPort.c 中实现 Native 方法:

核心代码:C/C++ 原生实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <jni.h>
#include <android/log.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <termios.h>

// 日志标签
#define LOG_TAG "SerialPort-JNI"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

/**
* JNI 函数命名规则:
* JNIEXPORT 返回类型 JNICALL Java_包名_类名_方法名
*
* @param env JNI 环境指针
* @param thiz 调用该方法的类实例(如果是静态方法则为类对象)
* @param path 串口设备路径
* @param baudrate 波特率
* @param flags 打开标志
* @param parity 校验位
* @param stopbits 停止位
* @param databits 数据位
* @return 返回 FileDescriptor 对象
*/
JNIEXPORT jobject JNICALL
Java_com_example_serialport_SerialPort_open(JNIEnv *env, jobject thiz,
jstring path, jint baudrate, jint flags,
jint parity, jint stopbits, jint databits) {

// 将 Java String 转换为 C 字符串
const char *path_utf = (*env)->GetStringUTFChars(env, path, NULL);

LOGD("Opening serial port: %s", path_utf);

// 打开串口设备
int fd = open(path_utf, O_RDWR | O_NOCTTY | O_NONBLOCK);

// 释放字符串内存
(*env)->ReleaseStringUTFChars(env, path, path_utf);

if (fd == -1) {
LOGE("Failed to open serial port: %s", strerror(errno));
return NULL;
}

// 配置串口参数
struct termios tios;
tcgetattr(fd, &tios);

// 设置波特率、数据位、停止位、校验位等...
// (具体实现省略)

tcsetattr(fd, TCSANOW, &tios);

// 创建 FileDescriptor 对象
jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");
jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "<init>", "()V");
jobject jFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor);

// 设置 fd 字段
jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");
(*env)->SetIntField(env, jFileDescriptor, descriptorID, (jint)fd);

return jFileDescriptor;
}

/**
* 关闭串口
*/
JNIEXPORT void JNICALL
Java_com_example_serialport_SerialPort_close(JNIEnv *env, jobject thiz) {
// 关闭串口的实现
LOGD("Closing serial port");
// 具体实现...
}

编程技巧:使用 Android Log 系统可以方便地调试 C/C++ 代码。

1
2
3
4
5

## JNI 函数命名规则

JNI 函数必须遵循特定的命名规则:

JNIEXPORT 返回类型 JNICALL Java_完整包名_类名_方法名

1
2
3
4
5
6
7
8

### 示例解析

对于包名为 `com.example.serialport`,类名为 `SerialPort`,方法名为 `open` 的函数:

```c
JNIEXPORT jobject JNICALL
Java_com_example_serialport_SerialPort_open(JNIEnv *env, jobject thiz, ...)
  • com.example.serialportcom_example_serialport
  • 包名中的点(.)替换为下划线(_)
  • 类名和方法名直接拼接

常见问题与注意事项

⚠️ 注意事项

  1. 内存管理:使用 GetStringUTFChars 后必须调用 ReleaseStringUTFChars
  2. 异常处理:JNI 调用可能产生异常,需要适当处理
  3. 线程安全:JNI 调用需要考虑线程安全问题
  4. 性能影响:频繁的 Java-Native 调用会影响性能

🔧 常见错误

  • UnsatisfiedLinkError:通常是库加载失败或函数签名不匹配
  • FindClass 失败:类名路径错误或类不存在
  • 内存泄漏:忘记释放 JNI 分配的内存

总结

Android JNI 开发虽然入门门槛较高,但掌握基本流程后就能够:

  1. 性能优化:将计算密集型任务移至 C/C++ 层
  2. 系统调用:直接调用 Linux 系统 API
  3. 代码复用:集成现有的 C/C++ 库

通过本教程的配置和示例,你应该能够开始自己的 JNI 开发之旅。建议从简单的函数开始,逐步深入学习更复杂的 JNI 特性。

参考资源