Android JNI 开发
1. 概述
1.1 功能简介
本文档介绍基于 M4-R2(RK3576)的 Android JNI(Java Native Interface)开发方法,涵盖 NDK 开发环境搭建、CMake 构建配置、JNI 函数注册方式、Java 与 C/C++ 数据类型映射,以及 RKNN NPU 推理的 JNI 集成示例。
1.2 应用场景
- 在 Android 应用中调用 C/C++ 高性能计算库
- 集成 RKNN NPU 推理(利用 RK3576 6 TOPS NPU)
- 复用已有的 C/C++ 算法代码
- 底层硬件访问(通过 JNI 调用系统库)
1.3 前置条件
- 已掌握 Android应用开发入门 基础
- 已安装 NDK 与 CMake
- 了解 C/C++ 与 Java 基础语法
2. NDK 开发环境
2.1 安装 NDK 与 CMake
在 Android Studio 的 SDK Manager → SDK Tools 中安装:
| 组件 | 要求 |
|---|---|
| NDK | r25c 或更新版本 |
| CMake | 3.22.1 或更新版本 |
| LLDB | 最新版本(调试用) |
2.2 配置项目
在 app/build.gradle.kts 中配置:
android {
defaultConfig {
ndk {
// 只编译 arm64 架构(RK3576 为 64 位仅系统)
abiFilters += listOf("arm64-v8a")
}
externalNativeBuild {
cmake {
cppFlags += "-std=c++17"
}
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
}2.3 CMakeLists.txt
app/src/main/cpp/CMakeLists.txt:
cmake_minimum_required(VERSION 3.22.1)
project("jnidemo")
add_library(
native-lib
SHARED
native-lib.cpp
)
# 链接标准库
find_library(log-lib log)
target_link_libraries(
native-lib
${log-lib}
)3. JNI 基础
3.1 加载动态库
在 Java/Kotlin 代码中加载:
package com.shimeta.m4r2
object NativeLib {
init {
System.loadLibrary("native-lib")
}
external fun stringFromJNI(): String
}3.2 静态 JNI 注册
C/C++ 端实现(函数名按 JNI 命名规范):
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_shimeta_m4r2_NativeLib_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++ (RK3576)";
return env->NewStringUTF(hello.c_str());
}3.3 动态 JNI 注册
使用 JNI_OnLoad 动态注册(推荐,便于管理多个函数):
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "JNIDemo"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
static jstring nativeHello(JNIEnv* env, jobject thiz) {
return env->NewStringUTF("Hello from dynamic JNI (RK3576)");
}
static const JNINativeMethod methods[] = {
{"nativeHello", "()Ljava/lang/String;", (void*)nativeHello},
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = nullptr;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass clazz = env->FindClass("com/shimeta/m4r2/NativeLib");
if (clazz == nullptr) return JNI_ERR;
env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0]));
LOGI("JNI_OnLoad: register natives ok");
return JNI_VERSION_1_6;
}3.4 数据类型映射
| Java 类型 | JNI 类型 | C/C++ 类型 |
|---|---|---|
| boolean | jboolean | unsigned char |
| byte | jbyte | signed char |
| char | jchar | unsigned short |
| short | jshort | short |
| int | jint | int |
| long | jlong | long long |
| float | jfloat | float |
| double | jdouble | double |
| String | jstring | const char* (经转换) |
| Object[] | jobjectArray | — |
| int[] | jintArray | jint* |
4. RKNN NPU 推理集成
参考说明:RK3576 提供 RKNN API(C/C++ 接口),通过 JNI 可在 Android 应用中调用 NPU 推理(6 TOPS,单核)。
4.1 加载 RKNN 库
init {
System.loadLibrary("rknn_api") // RKNN C API
System.loadLibrary("native-lib") // 自定义 JNI 库
}4.2 JNI 封装
#include <jni.h>
#include <rknn_api.h>
// 全局 RKNN 上下文
static rknn_context ctx = 0;
extern "C" JNIEXPORT jint JNICALL
Java_com_shimeta_m4r2_NpuHelper_initRKNN(
JNIEnv* env, jobject thiz, jstring model_path) {
const char* path = env->GetStringUTFChars(model_path, nullptr);
// 读取模型文件
FILE* fp = fopen(path, "rb");
if (!fp) return -1;
fseek(fp, 0, SEEK_END);
int model_size = ftell(fp);
fseek(fp, 0, SEEK_SET);
void* model_data = malloc(model_size);
fread(model_data, 1, model_size, fp);
fclose(fp);
// 初始化 RKNN 上下文
int ret = rknn_init(&ctx, model_data, model_size, 0, nullptr);
free(model_data);
env->ReleaseStringUTFChars(model_path, path);
return ret;
}
extern "C" JNIEXPORT jint JNICALL
Java_com_shimeta_m4r2_NpuHelper_runInference(
JNIEnv* env, jobject thiz, jbyteArray input, jint input_len,
jfloatArray output) {
jbyte* in = env->GetByteArrayElements(input, nullptr);
jfloat* out = env->GetFloatArrayElements(output, nullptr);
jsize out_len = env->GetArrayLength(output);
// 设置输入
rknn_input rk_in[1];
memset(rk_in, 0, sizeof(rk_in));
rk_in[0].index = 0;
rk_in[0].type = RKNN_TENSOR_UINT8;
rk_in[0].size = input_len;
rk_in[0].fmt = RKNN_TENSOR_NHWC;
rk_in[0].buf = in;
rknn_inputs_set(ctx, 1, rk_in);
// 执行推理
rknn_run(ctx, nullptr);
// 获取输出
rknn_output rk_out[1];
memset(rk_out, 0, sizeof(rk_out));
rk_out[0].index = 0;
rk_out[0].want_float = 1;
rknn_outputs_get(ctx, 1, rk_out, nullptr);
memcpy(out, rk_out[0].buf, out_len * sizeof(float));
rknn_outputs_release(ctx, 1, rk_out);
env->ReleaseByteArrayElements(input, in, 0);
env->ReleaseFloatArrayElements(output, out, 0);
return 0;
}
extern "C" JNIEXPORT void JNICALL
Java_com_shimeta_m4r2_NpuHelper_releaseRKNN(
JNIEnv* env, jobject thiz) {
if (ctx) {
rknn_destroy(ctx);
ctx = 0;
}
}4.3 Kotlin 调用
object NpuHelper {
init {
System.loadLibrary("rknn_api")
System.loadLibrary("native-lib")
}
external fun initRKNN(modelPath: String): Int
external fun runInference(input: ByteArray, inputLen: Int, output: FloatArray): Int
external fun releaseRKNN()
}4.4 调用示例
// 初始化 NPU
val ret = NpuHelper.initRKNN("/sdcard/model.rknn")
if (ret != 0) {
Log.e("NpuDemo", "RKNN init failed: $ret")
return
}
// 准备输入(224x224x3 uint8)
val input = ByteArray(224 * 224 * 3)
val output = FloatArray(1000) // 分类模型输出
// 执行推理
NpuHelper.runInference(input, input.size, output)
// 释放
NpuHelper.releaseRKNN()说明:RK3576 为单核 NPU,模型转换时
target_platform应指定为'rk3576',详见 RKNN Toolkit2 安装与使用。
5. 常见问题
5.1 UnsatisfiedLinkError
现象: 运行时提示找不到本地方法。
解决方案:
- 确认
abiFilters包含arm64-v8a - 确认库文件名与
System.loadLibrary一致 - 动态注册时确认包名/类名与
FindClass一致
5.2 JNI 崩溃(SIGSEGV)
解决方案:
- 检查数组 Get/Release 配对
- 确认本地引用及时释放(DeleteLocalRef)
- 使用
-fsanitize=address调试本地代码
5.3 RKNN 库加载失败
解决方案:
- 确认目标系统包含
librknnrt.so(/vendor/lib64/ 或 /system/lib64/) - 确认模型路径可读
- 查看 logcat 中 rknn 相关错误
6. 相关文档
- Android应用开发入门 — 应用开发基础
- RKNN Toolkit2 安装与使用 — RKNN 模型转换
- Android NPU使用指南 — NPU 使用
- Android HAL开发 — HAL 库集成
