① Android JNI 入门指南
🎯 学习目标:通过本教程,你将掌握 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(用于版本管理)
� 配置流程
graph TD
A[📱 创建 Android 项目] --> B[⚙️ 配置 Gradle]
B --> C[📁 创建 cpp 目录]
C --> D[📝 编写 C/C++ 代码]
D --> E[🔗 声明 Native 方法]
E --> F[📚 加载动态库]
F --> G[🎉 编译运行]
开发提示:建议按照上述流程逐步配置,每完成一步都进行测试验证。
⚙️ 详细配置
1. Gradle 配置
在 app/build.gradle.kts 文件中添加 NDK 和 CMake 配置:
配置文件:app/build.gradle.kts
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
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 官方首选语言
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 开发的开发者
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++ 原生实现
#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++ 代码。
## JNI 函数命名规则
JNI 函数必须遵循特定的命名规则:
JNIEXPORT 返回类型 JNICALL Java_完整包名_类名_方法名
### 示例解析
对于包名为 `com.example.serialport`,类名为 `SerialPort`,方法名为 `open` 的函数:
```c
JNIEXPORT jobject JNICALL
Java_com_example_serialport_SerialPort_open(JNIEnv *env, jobject thiz, ...)
com.example.serialport→com_example_serialport- 包名中的点(
.)替换为下划线(_) - 类名和方法名直接拼接
常见问题与注意事项
⚠️ 注意事项
- 内存管理:使用
GetStringUTFChars后必须调用ReleaseStringUTFChars - 异常处理:JNI 调用可能产生异常,需要适当处理
- 线程安全:JNI 调用需要考虑线程安全问题
- 性能影响:频繁的 Java-Native 调用会影响性能
🔧 常见错误
- UnsatisfiedLinkError:通常是库加载失败或函数签名不匹配
- FindClass 失败:类名路径错误或类不存在
- 内存泄漏:忘记释放 JNI 分配的内存
总结
Android JNI 开发虽然入门门槛较高,但掌握基本流程后就能够:
- 性能优化:将计算密集型任务移至 C/C++ 层
- 系统调用:直接调用 Linux 系统 API
- 代码复用:集成现有的 C/C++ 库
通过本教程的配置和示例,你应该能够开始自己的 JNI 开发之旅。建议从简单的函数开始,逐步深入学习更复杂的 JNI 特性。


