🎯 学习目标:通过本教程,你将掌握 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

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.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 特性。

参考资源