Android JNI 开发
1. 概述
1.1 功能简介
JNI(Java Native Interface)是 Android 系统中 Java 层与 C/C++ 原生代码交互的桥梁。本文档介绍本板(SMDT RK3588,Android 16 / API 36,平台相关结论在 2026-10-22 的实机上验证过)上 JNI 开发的方法,包括 NDK 集成、JNI 函数注册、原生库加载以及 RKNN NPU 推理的 JNI 集成方案。通用部分是参考说明,平台相关内容都按板上实测修订。
1.2 应用场景
- 高性能计算(C/C++ 加速,如图像处理、信号处理)
- RKNN NPU 推理集成(JNI 调用 RKNN C API,板载 RK1828 NPU)
- 硬件驱动访问(GPIO/UART/I2C 的 JNI 封装)
- 多媒体处理(OpenCV/MPP 的 JNI 集成)
1.3 工作原理
JNI 调用链路(参考说明):
Java/Kotlin App → JNI Interface → libnative.so → C/C++ 实现 → RKNN API / 系统调用2. 硬件准备
| 设备 | 型号/规格 | 数量 | 说明 |
|---|---|---|---|
| 开发主机 | PC (Windows/Linux/macOS) | 1 | 安装 Android Studio + NDK |
| 核心板 | C-3588LQ | 1 | RK3588 八核 |
| NPU | RK1828 PCIe NPU 卡 | 1 | 板载;rknn-smi 实测 Online(Bus 0004:41:00.0),5GB 专属内存 |
| 底板 | SHIMETAPI-M5-R2A | 1 |
3. 软件准备
3.1 通用工具
| 工具 | 版本建议 | 说明 |
|---|---|---|
| Android NDK | r25c 及以上 | 提供交叉编译工具链 |
| CMake | 3.22.1 及以上 | NDK 原生构建系统 |
| Android Studio | 最新稳定版 | IDE 开发环境 |
| JDK | 17 | 与 API 36 兼容 |
3.2 板端 RKNN 运行库
板端(Android 16 固件)/vendor/lib64 下经验证在位的 RKNN 相关库:
| 库 | 用途 |
|---|---|
librknn3_api.so | RK1828 NPU 推理 API(本板 AI 路线) |
librknn3_api_rkcp.so | RK1828 推理组件 |
librknnrt.so | RKNN Runtime(RKNN 通用运行时) |
librknnsmi_client.so | rknn-smi 客户端 |
rockchip.aidl.rknnsmi-V1-ndk.so 等 | AIDL HAL(rknnsmi / rknnmodeltest) |
板上已预置三个 AI 应用(com.rockchip.ai{service,launcher,asst}),rknn-smi info 可直接查看 NPU 状态。JNI 集成所需的头文件(rknn_api.h 等)与 API 文档以 SDK 树内 RKNN 开发包为准,模型转换工具 RKNN Toolkit2 见 ../ai-npu/ch01-android-npu-guide.md。
4. 操作步骤
4.1 NDK 安装与 CMake 配置
4.1.1 通过 SDK Manager 安装 NDK
在 Android Studio 中:Tools → SDK Manager → SDK Tools → NDK (Side by side),勾选对应版本后安装。
4.1.2 项目级 CMakeLists.txt 配置
在 app/ 目录下创建 CMakeLists.txt:
cmake_minimum_required(VERSION 3.22.1)
project("native-lib")
add_library(
native-lib
SHARED
src/main/cpp/native-lib.cpp
)
find_library(
log-lib
log
)
target_link_libraries(
native-lib
${log-lib}
)4.1.3 build.gradle 配置
在模块级 build.gradle.kts 中配置 CMake:
android {
...
defaultConfig {
externalNativeBuild {
cmake {
cppFlags("-std=c++17")
// 指定目标 ABI
abiFilters("arm64-v8a")
}
}
}
externalNativeBuild {
cmake {
path("CMakeLists.txt")
}
}
}4.2 JNI 函数注册方式
4.2.1 静态注册(JNI 命名约定)
Java 类中声明 native 方法:
public class NativeLib {
static {
System.loadLibrary("native-lib");
}
public native String stringFromJNI();
}对应的 C++ 实现(函数名遵循 JNI 命名规范):
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_NativeLib_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++ on RK3588";
return env->NewStringUTF(hello.c_str());
}4.2.2 动态注册(JNI_OnLoad)
推荐方式:性能更好,命名更灵活。
#include <jni.h>
static jstring nativeHello(JNIEnv* env, jobject thiz) {
return env->NewStringUTF("Hello from dynamic JNI on RK3588");
}
static const JNINativeMethod methods[] = {
{"nativeHello", "()Ljava/lang/String;", (void*)nativeHello},
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass clazz = env->FindClass("com/example/myapp/NativeLib");
if (clazz == nullptr) return JNI_ERR;
env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0]));
return JNI_VERSION_1_6;
}4.3 JNI 数据类型映射(参考说明)
| Java 类型 | JNI 类型 | 描述 |
|---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
String | jstring | Java 字符串 |
| 数组 | jbyteArray 等 | Java 数组 |
| 对象 | jobject | 任意 Java 对象 |
4.4 原生库交叉编译与打包(参考说明)
Android NDK 支持 arm64-v8a、armeabi-v7a、x86_64、x86 四种 ABI。本板实测 ro.product.cpu.abilist 仅 arm64-v8a(64 位单 ABI 系统),配置 abiFilters("arm64-v8a") 即可。
编译产物位于 app/build/intermediates/cmake/debug/obj/arm64-v8a/libnative-lib.so,由 Gradle 自动打包到 APK 中。
4.5 JNI 与 RKNN NPU 集成
RKNN 推理在 Android 端的典型集成方式是通过 JNI 调用 RKNN C API。本板架构:
Java App → JNI Interface → librknn3_api.so(RK1828 推理 API)→ AIDL/NPU 驱动 → RK1828 NPU(PCIe)注意:本板 AI 走 RK1828 PCIe NPU 卡(
rknn-smi实测 Online),不是 RK3588 SoC 内置 NPU。板端/vendor/lib64实测库为librknn3_api.so(RK1828 路线)与librknnrt.so(通用 Runtime)。
4.5.1 加载 RKNN 运行库
在 Java 端通过 System.loadLibrary 加载(库名以板端实测为准):
static {
System.loadLibrary("rknn3_api"); // RK1828 推理 API(本板实测库,/vendor/lib64/librknn3_api.so)
System.loadLibrary("native-lib"); // 应用层 JNI 库
}4.5.2 模型文件部署
RKNN 模型文件(.rknn 格式)在 PC 端通过 RKNN Toolkit2 转换后,部署到 Android 设备的指定路径(如 /data/local/tmp/ 或应用私有目录)。
4.5.3 JNI 层调用 RKNN API
参考流程(伪代码,接口签名以 SDK 内 RKNN API 文档为准):
// native-lib.cpp
#include <rknn_api.h>
extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_npu_NpuEngine_initModel(
JNIEnv* env, jobject thiz, jstring modelPath) {
const char* path = env->GetStringUTFChars(modelPath, nullptr);
rknn_context ctx;
int ret = rknn_init(&ctx, path, 0, 0, NULL);
env->ReleaseStringUTFChars(modelPath, path);
return (ret == RKNN_SUCC) ? JNI_TRUE : JNI_FALSE;
}
extern "C" JNIEXPORT jfloatArray JNICALL
Java_com_example_npu_NpuEngine_runInference(
JNIEnv* env, jobject thiz, jbyteArray input) {
// 获取输入数据
jbyte* inputData = env->GetByteArrayElements(input, nullptr);
jsize inputLen = env->GetArrayLength(input);
// 设置输入输出
rknn_input inputs[1];
inputs[0].index = 0;
inputs[0].buf = inputData;
inputs[0].size = inputLen;
inputs[0].pass_through = false;
inputs[0].type = RKNN_TENSOR_UINT8;
rknn_inputs_set(ctx, 1, inputs);
// 运行推理
rknn_run(ctx, nullptr);
// 获取输出
rknn_output outputs[1];
outputs[0].want_float = true;
rknn_outputs_get(ctx, 1, outputs);
// 转换输出到 Java 数组
float* result = (float*)outputs[0].buf;
int resultSize = outputs[0].size / sizeof(float);
jfloatArray jResult = env->NewFloatArray(resultSize);
env->SetFloatArrayRegion(jResult, 0, resultSize, result);
// 释放资源
rknn_outputs_release(ctx, 1, outputs);
env->ReleaseByteArrayElements(input, inputData, JNI_ABORT);
return jResult;
}以上代码为参考说明。RK1828 路线的具体 API 签名、模型加载方式与头文件,以 SDK 树内 RKNN 开发包(
rknn_api.h及配套文档)为准;动手前先用rknn-smi info确认 NPU Online。
5. 代码详解
5.1 完整 JNI 示例:应用层调用原生函数
Java 层(NativeLib.java):
package com.example.myapp;
public class NativeLib {
static {
System.loadLibrary("native-lib");
}
public native String stringFromJNI();
public native int addNumbers(int a, int b);
public native byte[] processImage(byte[] input, int width, int height);
}C++ 层(native-lib.cpp):
#include <jni.h>
#include <string>
#include <vector>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_NativeLib_stringFromJNI(
JNIEnv* env, jobject thiz) {
return env->NewStringUTF("RK3588 Native Library Ready");
}
extern "C" JNIEXPORT jint JNICALL
Java_com_example_myapp_NativeLib_addNumbers(
JNIEnv* env, jobject thiz, jint a, jint b) {
return a + b;
}
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_example_myapp_NativeLib_processImage(
JNIEnv* env, jobject thiz, jbyteArray input, jint width, jint height) {
jbyte* inputData = env->GetByteArrayElements(input, nullptr);
jsize inputLen = env->GetArrayLength(input);
// 图像处理逻辑(示例:灰度化)
std::vector<jbyte> output(inputData, inputData + inputLen);
// ... 处理代码 ...
jbyteArray result = env->NewByteArray(inputLen);
env->SetByteArrayRegion(result, 0, inputLen, output.data());
env->ReleaseByteArrayElements(input, inputData, JNI_ABORT);
return result;
}6. 常见问题
6.1 UnsatisfiedLinkError
- 确认
System.loadLibrary的库名与 CMakeLists.txt 中一致 - 确认
abiFilters包含arm64-v8a - 检查函数签名是否匹配
6.2 JNI 内存泄漏
GetByteArrayElements后必须调用ReleaseByteArrayElementsNewStringUTF返回的jstring由 JVM 管理,无需手动释放- 频繁调用的 JNI 函数注意缓存
jclass和jmethodID
6.3 JNI 线程问题
- JNI 调用只能在 Java 线程或已附加到 JVM 的 native 线程中执行
- native 线程需要调用
AttachCurrentThread附加到 JVM,使用完后DetachCurrentThread
7. 相关文档
| 文档 | 说明 |
|---|---|
| ch01-android-application-development.md | Android 应用开发基础 |
| ch01-android-hal-development.md | HAL 层开发 |
| ../ai-npu/ch01-android-npu-guide.md | NPU 推理与 RKNN 集成 |
| 01Linux系统/04application-development/ch01-c-cpp-application-development.md | C/C++ 开发参考 |
