🎯 学习目标:通过本教程,你将掌握 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| android { defaultConfig { ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") } }
externalNativeBuild { cmake { path = file("CMakeLists.txt") version = "3.18.1" } } }
|
注意:ABI 架构建议根据目标设备选择,过多的架构会增加 APK 体积。
2. CMake 配置
在项目根目录创建 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")
add_library( SerialPort SHARED src/main/cpp/SerialPort.h src/main/cpp/SerialPort.c )
target_link_libraries( SerialPort 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 { init { System.loadLibrary("SerialPort") } }
external fun open( path: String, baudrate: Int, flags: Int, parity: Int, stopbits: Int, databits: Int ): FileDescriptor?
external fun close() }
|
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 { static { System.loadLibrary("SerialPort"); }
public native FileDescriptor open(String path, int baudrate, int flags, int parity, int stopbits, int databits);
public native void close(); }
|
4. C/C++ 实现
在 app/src/main/cpp/SerialPort.c
中实现 Native 方法:
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__)
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) { 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); jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor"); jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "<init>", "()V"); jobject jFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor); 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.serialport
→ com_example_serialport
- 包名中的点(
.
)替换为下划线(_
)
- 类名和方法名直接拼接
常见问题与注意事项
⚠️ 注意事项
- 内存管理:使用
GetStringUTFChars
后必须调用 ReleaseStringUTFChars
- 异常处理:JNI 调用可能产生异常,需要适当处理
- 线程安全:JNI 调用需要考虑线程安全问题
- 性能影响:频繁的 Java-Native 调用会影响性能
🔧 常见错误
- UnsatisfiedLinkError:通常是库加载失败或函数签名不匹配
- FindClass 失败:类名路径错误或类不存在
- 内存泄漏:忘记释放 JNI 分配的内存
总结
Android JNI 开发虽然入门门槛较高,但掌握基本流程后就能够:
- 性能优化:将计算密集型任务移至 C/C++ 层
- 系统调用:直接调用 Linux 系统 API
- 代码复用:集成现有的 C/C++ 库
通过本教程的配置和示例,你应该能够开始自己的 JNI 开发之旅。建议从简单的函数开始,逐步深入学习更复杂的 JNI 特性。
参考资源