目录
[先打预防针:JNI 引用类型的核心规则,不遵守必崩](#先打预防针:JNI 引用类型的核心规则,不遵守必崩)
[一、JNI 字符串操作:jstring 全解析](#一、JNI 字符串操作:jstring 全解析)
[1. 核心原理:为什么 jstring 不能直接当 char * 用?](#1. 核心原理:为什么 jstring 不能直接当 char * 用?)
[2. 核心操作 1:Java→C++(jstring 转 char*)](#2. 核心操作 1:Java→C++(jstring 转 char*))
[RK3576 实战 demo:Java 传设备节点路径,C++ 打开设备](#RK3576 实战 demo:Java 传设备节点路径,C++ 打开设备)
[Java 层代码(MainActivity.java)](#Java 层代码(MainActivity.java))
[C++ 层代码(native-lib.cpp)](#C++ 层代码(native-lib.cpp))
[3. 核心操作 2:C++→Java(char * 转 jstring)](#3. 核心操作 2:C++→Java(char * 转 jstring))
[实战 demo:C++ 获取 RK3576 设备信息,返回给 Java](#实战 demo:C++ 获取 RK3576 设备信息,返回给 Java)
[4. 字符串操作新手必避的 5 个坑](#4. 字符串操作新手必避的 5 个坑)
[二、JNI 数组操作:全类型数组读写详解](#二、JNI 数组操作:全类型数组读写详解)
[1. 数组操作的核心规则](#1. 数组操作的核心规则)
[2. 核心方法:数组通用操作](#2. 核心方法:数组通用操作)
[重点讲 Release 方法的 mode 参数](#重点讲 Release 方法的 mode 参数)
[3. RK3576 实战 demo1:批量处理传感器采样数据](#3. RK3576 实战 demo1:批量处理传感器采样数据)
[Java 层代码](#Java 层代码)
[C++ 层代码](#C++ 层代码)
[4. RK3576 实战 demo2:图像处理字节流操作](#4. RK3576 实战 demo2:图像处理字节流操作)
[Java 层代码](#Java 层代码)
[C++ 层代码](#C++ 层代码)
[5. 数组操作新手必避的 4 个坑](#5. 数组操作新手必避的 4 个坑)
[三、JNI 对象操作:jobject 属性访问与方法调用](#三、JNI 对象操作:jobject 属性访问与方法调用)
[1. 对象操作的核心原理](#1. 对象操作的核心原理)
[2. 核心操作 1:C++ 访问 Java 对象的属性](#2. 核心操作 1:C++ 访问 Java 对象的属性)
[RK3576 实战 demo:读取传感器数据对象](#RK3576 实战 demo:读取传感器数据对象)
[Java 层代码:定义实体类 + native 方法](#Java 层代码:定义实体类 + native 方法)
[C++ 层代码:读取并修改对象属性](#C++ 层代码:读取并修改对象属性)
[3. 核心操作 2:C++ 调用 Java 对象的方法](#3. 核心操作 2:C++ 调用 Java 对象的方法)
[RK3576 实战 demo:C++ 调用 Java 回调方法](#RK3576 实战 demo:C++ 调用 Java 回调方法)
[Java 层代码:定义回调接口 + native 方法](#Java 层代码:定义回调接口 + native 方法)
[C++ 层代码:调用 Java 回调方法](#C++ 层代码:调用 Java 回调方法)
[4. 对象操作新手必避的 5 个坑](#4. 对象操作新手必避的 5 个坑)
[报错 1:JNI DETECTED ERROR IN APPLICATION: use of deleted local reference](#报错 1:JNI DETECTED ERROR IN APPLICATION: use of deleted local reference)
[报错 2:NoSuchFieldError/NoSuchMethodError](#报错 2:NoSuchFieldError/NoSuchMethodError)
[报错 3:Native crash at /system/lib64/libc.so,野指针崩溃](#报错 3:Native crash at /system/lib64/libc.so,野指针崩溃)
[报错 4:内存泄漏,APP 跑久了 OOM 崩溃](#报错 4:内存泄漏,APP 跑久了 OOM 崩溃)
[本章总结 + 下章预告](#本章总结 + 下章预告)
前言
哈喽各位兄弟们,我是你们的黒漂技术佬!
上一章咱们啃完了 JNI 基本类型映射、方法签名、静态注册的核心内容,后台一堆兄弟报喜:"佬哥,我终于能给 C++ 传 GPIO 编号、控制 LED 亮灭了!" 但同时也收到了新的灵魂拷问:"佬哥,我想给 C++ 传设备节点路径/dev/i2c-2,直接把 String 扔进去直接崩了,这是为啥?""我要把传感器 100 组采样数据传给 C++ 做滤波,用 int 数组传进去,要么数据全乱了,要么直接闪退,咋整?""我定义了一个传感器数据的 Java 实体类,想让 C++ 读完硬件数据直接封装好返回,结果根本拿不到对象里的属性,人都麻了!"
懂了懂了!这些问题,是 JNI 新手从 "能跑通" 到 "能干活" 的必经之坎。上一章的基本类型只能传单个数值,但咱们做 RK3576 底层开发,90% 的场景都要和字符串、数组、自定义对象打交道:传设备路径、读写 I2C 字节流、批量处理传感器数据、封装硬件状态对象,全靠这些引用类型。
今天这一章,佬哥我还是老规矩:大白话讲原理 + 保姆级步骤 + RK3576 实战场景 + 踩坑急救指南,把字符串、数组、对象操作给你扒得明明白白,所有 demo 全是咱们后续驱动开发直接能用的代码,学完就能落地,再也不会因为引用类型操作不当崩到心态爆炸!
先打预防针:JNI 引用类型的核心规则,不遵守必崩
上一章咱们提过,JNI 类型分为基本类型和引用类型,这里必须先给新手把核心规则焊死在脑子里,不然你写的代码跑 10 次有 9 次会崩,剩下 1 次也藏着内存泄漏的雷。
表格
| 特性 | 基本类型(jint/jboolean 等) | 引用类型(jstring/jarray/jobject 等) |
|---|---|---|
| 操作方式 | 直接赋值、直接运算,和 C++ 原生类型无缝兼容 | 不能直接操作!必须通过JNIEnv*提供的专用方法处理 |
| 内存管理 | 栈上分配,方法结束自动释放,无需手动管理 | 堆上分配,受 Java GC 管理,手动获取的引用必须手动释放 |
| 生命周期 | 和方法调用绑定,方法结束就失效 | 有严格的引用生命周期规则,乱用会导致内存泄漏、GC 崩溃 |
用大白话类比:基本类型就像你手里的现金,拿到就能直接花,用完就没了,不用管后续;引用类型就像你从快递站取的包裹,里面装着你要的东西,但你不能直接手撕包裹,必须用快递站给的专用工具打开,用完之后还要把包装垃圾扔掉,不然快递站(Java 堆)会被垃圾堆满,最终爆仓(OOM 崩溃)。
而JNIEnv*,就是 JNI 给你提供的「专用工具包」,所有引用类型的操作,全靠它里面的方法实现。这一章的所有内容,本质上就是教你怎么用这个工具包,正确打开、使用、释放这些引用类型的包裹。
一、JNI 字符串操作:jstring 全解析
字符串是咱们开发中第一个要搞定的引用类型:传设备节点路径、设备名称、日志信息、文件路径,全靠jstring。新手 90% 的崩溃,都是因为把jstring直接当成 C++ 的char*来用。
1. 核心原理:为什么 jstring 不能直接当 char * 用?
Java 的String和 C++ 的char*,底层编码完全不一样:
- Java 的
String是UTF-16 编码,每个字符占 2 字节,中文、英文都是 2 字节; - C++ 的
char*是UTF-8 编码,英文占 1 字节,中文占 3 字节,是 Linux 系统、C/C++ 开发的通用编码。
你直接把 UTF-16 的 jstring 当成 UTF-8 的 char * 用,就像把中文书直接给只会英文的人看,全是乱码,不崩才怪。
JNI 专门提供了一套方法,帮你完成jstring和char*的互转,同时处理编码问题,咱们只需要按步骤调用就行。
2. 核心操作 1:Java→C++(jstring 转 char*)
这是最常用的场景:Java 层把设备路径、文件名传给 C++ 层,C++ 层用来打开设备、读写文件。
标准步骤(一步都不能少)
- 用
GetStringUTFChars获取 UTF-8 编码的 char * 指针; - 校验指针是否为空,避免空指针崩溃;
- 使用 char * 完成你的业务逻辑;
- 必须用
ReleaseStringUTFChars释放指针,不然会造成内存泄漏!
方法详解
cpp
运行
// 方法原型
const char* GetStringUTFChars(JNIEnv *env, jstring str, jboolean *isCopy);
void ReleaseStringUTFChars(JNIEnv *env, jstring str, const char* chars);
- 参数说明:
env:JNI 环境指针,固定参数;str:Java 传进来的 jstring 对象;isCopy:输出参数,JNI_TRUE 表示返回的是字符串的拷贝,JNI_FALSE 表示返回的是 Java 原始字符串的直接指针。新手直接传 NULL 就行,不用管这个参数。
- 注意:
GetStringUTFChars可能会返回 NULL!如果 Java 传进来的是 null,这里会返回 NULL,必须做空指针校验,不然直接崩溃。
RK3576 实战 demo:Java 传设备节点路径,C++ 打开设备
咱们结合后续驱动开发的场景,写一个实战代码:Java 传 I2C 设备节点路径/dev/i2c-2给 C++,C++ 打开设备,返回是否打开成功。
Java 层代码(MainActivity.java)
java
运行
// 定义native方法:传入设备路径,返回设备是否打开成功
public native boolean openDevice(String devicePath);
// onCreate里调用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 省略布局绑定代码...
// 传入RK3576的I2C设备节点路径
boolean isOpen = openDevice("/dev/i2c-2");
Log.d("Heipiao_RK3576", "设备打开结果:" + isOpen);
}
C++ 层代码(native-lib.cpp)
cpp
运行
#include <jni.h>
#include <string>
#include <android/log.h>
// Linux系统调用,打开设备节点需要
#include <fcntl.h>
#include <unistd.h>
#define LOG_TAG "Heipiao_RK3576_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__)
extern "C" JNIEXPORT jboolean JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_openDevice(JNIEnv *env, jobject thiz, jstring device_path) {
// 步骤1:获取char*指针
const char *device_path_c = env->GetStringUTFChars(device_path, NULL);
// 步骤2:必须做空指针校验!
if (device_path_c == NULL) {
LOGE("设备路径为空,获取失败");
return JNI_FALSE;
}
// 步骤3:业务逻辑:打开设备节点
LOGD("收到设备路径:%s", device_path_c);
// open系统调用,只读方式打开设备
int fd = open(device_path_c, O_RDONLY);
if (fd < 0) {
LOGE("设备打开失败,路径:%s", device_path_c);
// 步骤4:哪怕打开失败,也要释放指针!不然内存泄漏
env->ReleaseStringUTFChars(device_path, device_path_c);
return JNI_FALSE;
}
LOGD("设备打开成功,文件描述符fd=%d", fd);
// 用完设备记得关闭,这里只是demo,实际开发中fd要保存起来后续用
close(fd);
// 步骤4:必须释放指针!
env->ReleaseStringUTFChars(device_path, device_path_c);
return JNI_TRUE;
}
划重点!不管业务逻辑成功还是失败,只要调用了 GetStringUTFChars,就必须调用 ReleaseStringUTFChars 释放,哪怕中途 return 了,也要先释放再 return,不然一次调用泄漏一点内存,跑久了 APP 直接 OOM 崩溃。
3. 核心操作 2:C++→Java(char * 转 jstring)
这个场景也很常用:C++ 层读取设备信息、传感器数据,转换成字符串返回给 Java 层显示。
核心方法
cpp
运行
jstring NewStringUTF(JNIEnv *env, const char *bytes);
- 作用:把 C++ 的 UTF-8 编码 char * 字符串,转换成 Java 的 jstring 对象,直接返回给 Java 层。
- 注意:传入的 char * 必须是 UTF-8 编码,不然 Java 层会拿到乱码。
实战 demo:C++ 获取 RK3576 设备信息,返回给 Java
cpp
运行
// Java层定义native方法
public native String getDeviceInfo();
// C++层实现
extern "C" JNIEXPORT jstring JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_getDeviceInfo(JNIEnv *env, jobject thiz) {
// 模拟从硬件读取设备信息,实际开发中可以从sysfs读取RK3576的芯片信息
char device_info[256];
sprintf(device_info, "芯片型号:RK3576 | 架构:arm64-v8a | 内核版本:Linux 5.10 | 开发者:黒漂技术佬");
// 直接转换成jstring返回给Java
return env->NewStringUTF(device_info);
}
4. 字符串操作新手必避的 5 个坑
- 不释放 GetStringUTFChars 获取的指针:内存泄漏的重灾区,记住:有 Get 必有 Release,成对出现!
- 不做空指针校验:Java 传 null 进来,GetStringUTFChars 会返回 NULL,直接用会触发空指针崩溃。
- 把 jstring 直接当 char * 用:编码不匹配,要么乱码,要么直接崩溃,必须用 JNI 方法转换。
- ReleaseStringUTFChars 之后还继续使用 char * 指针:释放之后指针就失效了,再用就是野指针,必崩。
- 中文乱码:确保 C++ 的 char * 是 UTF-8 编码,别用 GBK 编码,不然 Java 层拿到的全是问号。
二、JNI 数组操作:全类型数组读写详解
数组是咱们 RK3576 底层开发里用得最多的引用类型,没有之一:
- 传感器批量采样数据:int 数组、float 数组;
- I2C/SPI 读写的字节流:byte 数组;
- 摄像头、RGA 图像处理的帧数据:byte 数组、int 数组;
- GPIO 批量配置参数:int 数组。
JNI 里的数组分为基本类型数组 (jintArray、jbyteArray 等)和对象数组(jobjectArray),咱们先讲 90% 场景都会用到的基本类型数组,对象数组放在后面对象操作里讲。
1. 数组操作的核心规则
- 基本类型数组不能直接用 C++ 的指针访问,必须通过 JNI 提供的方法获取数组元素;
- 获取数组元素有两种模式:拷贝模式 (JNI 把 Java 数组拷贝到 C++ 堆里)和直接指针模式(直接返回 Java 数组的内存地址),不用关心底层用了哪种模式,按步骤调用就行;
- 有 Get 必有 Release,获取的数组指针必须手动释放,不然内存泄漏;
- JNI 不会帮你做数组越界检查,你自己要保证不超过数组长度,越界直接崩溃。
2. 核心方法:数组通用操作
不管是 int 数组、byte 数组还是 float 数组,操作方法都是一套,只是把类型名换一下,非常好记。
表格
| 通用方法原型 | 作用 |
|---|---|
| jsize GetArrayLength(JNIEnv *env, jarray array) | 获取数组的长度,和 Java 的 array.length 一样 |
| <Type>* Get<Type>ArrayElements(JNIEnv *env, <Type>Array array, jboolean *isCopy) | 获取数组元素的指针,比如 GetIntArrayElements、GetByteArrayElements |
| void Release<Type>ArrayElements(JNIEnv env, <Type>Array array, <Type> elems, jint mode) | 释放数组指针,和 Get 方法成对出现 |
| void Get<Type>ArrayRegion(JNIEnv env, <Type>Array array, jsize start, jsize len, <Type> buf) | 拷贝数组的指定区间到 C++ 的缓冲区里,适合只读场景 |
| void Set<Type>ArrayRegion(JNIEnv env, <Type>Array array, jsize start, jsize len, const <Type> buf) | 把 C++ 缓冲区里的数据写回 Java 数组的指定区间 |
重点讲 Release 方法的 mode 参数
很多新手搞不懂这个 mode,这里大白话讲清楚,3 个可选值:
0:默认值,把 C++ 里对数组的修改同步回 Java 数组,同时释放 C++ 的指针;JNI_COMMIT:把修改同步回 Java 数组,但不释放指针,适合需要多次修改数组的场景;JNI_ABORT:不把修改同步回 Java 数组,只释放指针,适合只读数组的场景,性能更高。
3. RK3576 实战 demo1:批量处理传感器采样数据
场景:Java 层把传感器 100 组采样的 int 数组传给 C++,C++ 做均值滤波,返回滤波后的平均值,这个是咱们做传感器数据处理天天要用的逻辑。
Java 层代码
java
运行
// native方法:传入传感器采样数组,返回滤波后的平均值
public native float calcSensorAvg(int[] sensorData);
// onCreate里调用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 模拟100组传感器采样数据
int[] sensorData = new int[100];
for (int i = 0; i < 100; i++) {
sensorData[i] = (int) (Math.random() * 100 + 20); // 模拟20-120的采样值
}
float avg = calcSensorAvg(sensorData);
Log.d("Heipiao_RK3576", "传感器滤波后平均值:" + avg);
}
C++ 层代码
cpp
运行
extern "C" JNIEXPORT jfloat JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_calcSensorAvg(JNIEnv *env, jobject thiz, jintArray sensor_data) {
// 步骤1:获取数组长度
jsize array_len = env->GetArrayLength(sensor_data);
LOGD("收到传感器数据,数组长度:%d", array_len);
// 步骤2:获取数组元素指针
jint *data_ptr = env->GetIntArrayElements(sensor_data, NULL);
if (data_ptr == NULL) {
LOGE("获取数组指针失败");
return -1;
}
// 步骤3:业务逻辑:计算平均值
jint sum = 0;
for (int i = 0; i < array_len; i++) {
sum += data_ptr[i];
}
jfloat avg = (jfloat) sum / array_len;
LOGD("传感器数据总和:%d,平均值:%.2f", sum, avg);
// 步骤4:释放数组指针,只读场景用JNI_ABORT,不用同步修改,性能更高
env->ReleaseIntArrayElements(sensor_data, data_ptr, JNI_ABORT);
// 返回结果
return avg;
}
4. RK3576 实战 demo2:图像处理字节流操作
场景:Java 层把摄像头的一帧图像 byte 数组传给 C++,C++ 做简单的灰度处理,把处理后的数据写回 Java 数组,这个是咱们后面讲 RGA 硬件加速的基础。
Java 层代码
java
运行
// native方法:处理图像字节数组,直接修改原数组
public native void processImage(byte[] imageData, int width, int height);
// onCreate里调用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 模拟640*480的RGB图像字节数组
int width = 640;
int height = 480;
byte[] imageData = new byte[width * height * 3];
// 模拟填充图像数据...
// 调用native方法处理图像
processImage(imageData, width, height);
Log.d("Heipiao_RK3576", "图像处理完成");
}
C++ 层代码
cpp
运行
extern "C" JNIEXPORT void JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_processImage(JNIEnv *env, jobject thiz, jbyteArray image_data, jint width, jint height) {
jsize data_len = env->GetArrayLength(image_data);
LOGD("收到图像数据,尺寸:%dx%d,数据长度:%d", width, height, data_len);
// 获取字节数组指针
jbyte *image_ptr = env->GetByteArrayElements(image_data, NULL);
if (image_ptr == NULL) {
LOGE("获取图像数据指针失败");
return;
}
// 业务逻辑:简单的RGB转灰度处理
int pixel_count = width * height;
for (int i = 0; i < pixel_count; i++) {
// 每个像素3个字节:R、G、B
int r = image_ptr[i * 3] & 0xFF;
int g = image_ptr[i * 3 + 1] & 0xFF;
int b = image_ptr[i * 3 + 2] & 0xFF;
// 灰度公式:Gray = R*0.299 + G*0.587 + B*0.114
jbyte gray = (jbyte) (r * 0.299 + g * 0.587 + b * 0.114);
// 把RGB三个通道都设为灰度值,实现灰度图
image_ptr[i * 3] = gray;
image_ptr[i * 3 + 1] = gray;
image_ptr[i * 3 + 2] = gray;
}
LOGD("图像处理完成");
// 释放指针,用默认mode 0,把修改同步回Java数组
env->ReleaseByteArrayElements(image_data, image_ptr, 0);
}
5. 数组操作新手必避的 4 个坑
- 不释放数组指针:和字符串一样,有 Get 必有 Release,成对出现,不然内存泄漏;
- 数组越界访问:JNI 不会帮你检查数组长度,循环的时候一定要用 GetArrayLength 获取的长度,别硬编码,越界直接崩溃;
- 空指针校验缺失:Java 传 null 数组进来,Get 方法会返回 NULL,直接用必崩;
- Release 之后还使用指针:释放之后指针就失效了,再用就是野指针,大概率触发 Native 崩溃,很难排查。
三、JNI 对象操作:jobject 属性访问与方法调用
咱们做实际开发的时候,不会只传单个数值或者数组,更多时候会用自定义的 Java 实体类封装数据,比如:
- 传感器数据类:封装温度、湿度、时间戳、设备 ID;
- 设备信息类:封装设备名称、硬件版本、固件版本、状态;
- 回调接口:C++ 层收到硬件中断,调用 Java 层的回调方法更新 UI。
这些场景,都需要 JNI 操作 Java 对象(jobject),核心就是两个操作:访问对象的属性 、调用对象的方法。
1. 对象操作的核心原理
Java 的对象,本质上是「属性 + 方法」的集合,JNI 要操作对象,必须先拿到两个东西:
- jclass:对象对应的 Java 类的 Class 对象,相当于对象的 "设计图纸";
- FieldID/MethodID:属性 / 方法的唯一标识,相当于图纸上的 "零件编号",通过这个编号才能找到对应的属性 / 方法。
划重点!FieldID 和 MethodID 是可以缓存的,不要每次调用 JNI 函数都去获取,不然会严重影响性能,尤其是高频调用的场景(比如每帧图像处理、传感器高频采样),这个是新手最容易忽略的性能优化点。
2. 核心操作 1:C++ 访问 Java 对象的属性
标准步骤
- 用
GetObjectClass获取对象对应的 jclass; - 用
GetFieldID获取属性的 FieldID,需要传入属性名、属性的签名(上一章讲的类型签名); - 用
Get<Type>Field获取属性的值,传入对象和 FieldID; - 如果要修改属性的值,用
Set<Type>Field。
方法详解
cpp
运行
// 获取对象的Class对象
jclass GetObjectClass(JNIEnv *env, jobject obj);
// 获取属性的FieldID
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
// 获取/设置属性值,对应不同类型
jint GetIntField(JNIEnv *env, jobject obj, jfieldID fieldID);
void SetIntField(JNIEnv *env, jobject obj, jfieldID fieldID, jint value);
jfloat GetFloatField(JNIEnv *env, jobject obj, jfieldID fieldID);
void SetFloatField(JNIEnv *env, jobject obj, jfieldID fieldID, jfloat value);
jboolean GetBooleanField(JNIEnv *env, jobject obj, jfieldID fieldID);
void SetBooleanField(JNIEnv *env, jobject obj, jfieldID fieldID, jboolean value);
jstring GetObjectField(JNIEnv *env, jobject obj, jfieldID fieldID);
void SetObjectField(JNIEnv *env, jobject obj, jfieldID fieldID, jobject value);
RK3576 实战 demo:读取传感器数据对象
咱们先在 Java 层定义一个传感器数据的实体类,然后把对象传给 C++,C++ 读取对象里的属性,做数据校验,修改状态属性。
Java 层代码:定义实体类 + native 方法
java
运行
// 传感器数据实体类
public class SensorData {
// 温度
public float temperature;
// 湿度
public float humidity;
// 设备ID
public String deviceId;
// 数据是否有效
public boolean isValid;
public SensorData(float temperature, float humidity, String deviceId) {
this.temperature = temperature;
this.humidity = humidity;
this.deviceId = deviceId;
this.isValid = false;
}
}
// MainActivity里定义native方法:校验传感器数据
public native void checkSensorData(SensorData data);
// onCreate里调用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 创建传感器数据对象
SensorData data = new SensorData(25.6f, 45.2f, "RK3576_I2C1_SHT30");
// 调用native方法校验数据
checkSensorData(data);
Log.d("Heipiao_RK3576", "数据校验结果:" + data.isValid);
}
C++ 层代码:读取并修改对象属性
cpp
运行
extern "C" JNIEXPORT void JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_checkSensorData(JNIEnv *env, jobject thiz, jobject sensor_data) {
// 步骤1:获取对象的jclass
jclass sensor_class = env->GetObjectClass(sensor_data);
if (sensor_class == NULL) {
LOGE("获取Class对象失败");
return;
}
// 步骤2:获取各个属性的FieldID
// 格式:GetFieldID(jclass, 属性名, 属性签名)
jfieldID temperature_fid = env->GetFieldID(sensor_class, "temperature", "F");
jfieldID humidity_fid = env->GetFieldID(sensor_class, "humidity", "F");
jfieldID device_id_fid = env->GetFieldID(sensor_class, "deviceId", "Ljava/lang/String;");
jfieldID is_valid_fid = env->GetFieldID(sensor_class, "isValid", "Z");
// 校验FieldID是否获取成功,只要有一个为NULL,说明属性名/签名写错了
if (temperature_fid == NULL || humidity_fid == NULL || device_id_fid == NULL || is_valid_fid == NULL) {
LOGE("获取FieldID失败");
return;
}
// 步骤3:获取属性的值
jfloat temperature = env->GetFloatField(sensor_data, temperature_fid);
jfloat humidity = env->GetFloatField(sensor_data, humidity_fid);
jstring device_id_jstr = (jstring) env->GetObjectField(sensor_data, device_id_fid);
// 转换设备ID字符串
const char *device_id_c = env->GetStringUTFChars(device_id_jstr, NULL);
LOGD("收到传感器数据:设备ID=%s,温度=%.2f℃,湿度=%.2f%%", device_id_c, temperature, humidity);
// 步骤4:业务逻辑:数据校验,温度在-40~125℃,湿度在0~100%为有效
jboolean is_valid = JNI_FALSE;
if (temperature >= -40 && temperature <= 125 && humidity >= 0 && humidity <= 100) {
is_valid = JNI_TRUE;
LOGD("传感器数据有效");
} else {
LOGE("传感器数据超出范围,无效");
}
// 步骤5:修改对象的isValid属性
env->SetBooleanField(sensor_data, is_valid_fid, is_valid);
// 释放字符串指针
env->ReleaseStringUTFChars(device_id_jstr, device_id_c);
}
3. 核心操作 2:C++ 调用 Java 对象的方法
这个场景最常用的就是回调:比如 C++ 层在后台线程监听硬件中断、传感器数据,收到数据之后,调用 Java 层的方法,更新 UI、触发业务逻辑。
标准步骤
- 用
GetObjectClass获取对象的 jclass; - 用
GetMethodID获取方法的 MethodID,需要传入方法名、方法签名(上一章讲的方法签名,必须用 javap 自动生成!); - 用
Call<Type>Method调用方法,传入对象、MethodID 和方法参数。
方法详解
cpp
运行
// 获取方法的MethodID
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
// 调用方法,对应不同的返回值类型
void CallVoidMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...); // 无返回值
jint CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jboolean CallBooleanMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
jobject CallObjectMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...);
RK3576 实战 demo:C++ 调用 Java 回调方法
场景:Java 层给 C++ 设置一个回调接口,C++ 层模拟收到传感器数据,调用 Java 的回调方法,把数据传给 Java 层更新 UI,这个是咱们做硬件开发的标准架构。
Java 层代码:定义回调接口 + native 方法
java
运行
// 传感器数据回调接口
public interface SensorCallback {
// 收到传感器数据的回调方法
void onSensorDataReceived(float temperature, float humidity);
// 传感器出错的回调方法
void onSensorError(String errorMsg);
}
// MainActivity里实现回调接口
public class MainActivity extends AppCompatActivity implements SensorCallback {
static {
System.loadLibrary("native-lib");
}
private TextView tvTemperature;
private TextView tvHumidity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 省略布局绑定代码...
tvTemperature = findViewById(R.id.tv_temperature);
tvHumidity = findViewById(R.id.tv_humidity);
// 设置回调给C++层
setSensorCallback(this);
// 启动传感器模拟读取
startSensorRead();
}
// native方法:设置回调接口
public native void setSensorCallback(SensorCallback callback);
// native方法:启动传感器读取
public native void startSensorRead();
// 实现回调方法:收到数据更新UI
@Override
public void onSensorDataReceived(float temperature, float humidity) {
runOnUiThread(() -> {
tvTemperature.setText(String.format("温度:%.2f℃", temperature));
tvHumidity.setText(String.format("湿度:%.2f%%", humidity));
});
}
// 实现回调方法:出错提示
@Override
public void onSensorError(String errorMsg) {
runOnUiThread(() -> {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
});
}
}
C++ 层代码:调用 Java 回调方法
cpp
运行
// 全局变量,缓存回调对象和MethodID,实际开发中建议用弱引用,避免内存泄漏
jobject g_sensor_callback = NULL;
jmethodID g_on_data_received_mid = NULL;
jmethodID g_on_error_mid = NULL;
// 设置回调的native方法
extern "C" JNIEXPORT void JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_setSensorCallback(JNIEnv *env, jobject thiz, jobject callback) {
// 因为回调对象要在全局使用,必须创建全局引用,不然方法结束后会被GC回收
if (g_sensor_callback != NULL) {
env->DeleteGlobalRef(g_sensor_callback);
}
g_sensor_callback = env->NewGlobalRef(callback);
// 获取回调接口的Class对象
jclass callback_class = env->GetObjectClass(callback);
// 获取两个回调方法的MethodID,用javap自动生成的签名,绝对不会错
g_on_data_received_mid = env->GetMethodID(callback_class, "onSensorDataReceived", "(FF)V");
g_on_error_mid = env->GetMethodID(callback_class, "onSensorError", "(Ljava/lang/String;)V");
LOGD("传感器回调设置成功");
}
// 启动传感器读取的native方法,模拟后台读取数据
extern "C" JNIEXPORT void JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_startSensorRead(JNIEnv *env, jobject thiz) {
if (g_sensor_callback == NULL || g_on_data_received_mid == NULL) {
LOGE("回调未设置,无法启动传感器读取");
return;
}
// 模拟10次传感器数据读取,实际开发中用子线程循环读取
for (int i = 0; i < 10; i++) {
// 模拟生成温度湿度数据
float temperature = 20.0f + (float) (rand() % 200) / 10.0f; // 20-40℃
float humidity = 30.0f + (float) (rand() % 400) / 10.0f; // 30-70%
// 调用Java的onSensorDataReceived回调方法
env->CallVoidMethod(g_sensor_callback, g_on_data_received_mid, temperature, humidity);
LOGD("传感器数据已回调,温度=%.2f,湿度=%.2f", temperature, humidity);
// 模拟1秒读取一次
sleep(1);
}
// 模拟读取结束,调用错误回调
env->CallVoidMethod(g_sensor_callback, g_on_error_mid, env->NewStringUTF("传感器读取结束"));
}
4. 对象操作新手必避的 5 个坑
- 属性名 / 方法名写错,或者签名写错 :99% 的
NoSuchFieldError/NoSuchMethodError都是这个原因,绝对不要手写签名,用 javap 命令自动生成! - 不缓存 FieldID/MethodID:每次调用都去获取 ID,会严重影响性能,尤其是高频调用的场景,一定要缓存起来。
- 局部引用全局使用 :方法里拿到的 jobject、jclass 都是局部引用,方法结束就会失效,要全局使用必须用
NewGlobalRef创建全局引用,不然会被 GC 回收,导致野指针崩溃。 - 全局引用不释放 :用
NewGlobalRef创建的全局引用,必须用DeleteGlobalRef释放,不然永远不会被 GC 回收,造成严重的内存泄漏。 - 在子线程里调用 Java 方法 :JNIEnv 是线程绑定的,子线程里不能直接用主线程的 JNIEnv,必须用
AttachCurrentThread把线程绑定到 JVM,不然直接崩溃,这个咱们后面线程章节会细讲。
四、新手踩坑急救站:本章高频报错解决方案
报错 1:JNI DETECTED ERROR IN APPLICATION: use of deleted local reference
原因 :使用了已经失效的局部引用,比如把方法里的局部 jobject 保存到全局变量,方法结束后再用,已经被 GC 回收了。解决方案 :要全局使用的引用,必须用NewGlobalRef创建全局引用,用完用DeleteGlobalRef释放。
报错 2:NoSuchFieldError/NoSuchMethodError
原因 :99% 是属性名 / 方法名写错了,或者签名写错了,比如类类型签名结尾的分号忘了,或者参数顺序错了。解决方案 :绝对不要手写签名,用javap -s -p 类名命令自动生成,复制粘贴绝对不会错。
报错 3:Native crash at /system/lib64/libc.so,野指针崩溃
原因:大概率是以下几种情况:
- 访问了已经 Release 的字符串 / 数组指针;
- 数组越界访问;
- 空指针没有校验,直接使用 NULL 指针。解决方案:所有 Get 方法返回的指针都做空指针校验,有 Get 必有 Release,数组循环严格控制长度。
报错 4:内存泄漏,APP 跑久了 OOM 崩溃
原因:
- GetStringUTFChars、GetArrayElements 获取的指针没有 Release;
- 全局引用没有 DeleteGlobalRef 释放;
- 循环里频繁创建局部引用,没有手动 DeleteLocalRef。解决方案 :所有引用都成对处理,有申请必有释放,循环里的局部引用用完就用
env->DeleteLocalRef()释放。
本章总结 + 下章预告
【本章总结】
今天这一章,咱们彻底啃完了 JNI 引用类型的核心操作,核心就 4 件事:
- 搞懂了引用类型和基本类型的核心区别,记住了「有 Get 必有 Release」的铁律,从根源上避免内存泄漏;
- 掌握了 jstring 的完整操作,能自由实现 Java 和 C++ 之间的字符串互转,传设备路径、设备信息再也不会崩;
- 掌握了基本类型数组的读写操作,能处理传感器批量数据、图像字节流,为后续的硬件操作、RGA 图像处理打下了基础;
- 掌握了 Java 对象的属性访问和方法调用,能实现 C++ 到 Java 的回调,完成硬件数据的实时 UI 更新,搞定了实际开发的核心架构。
【下章预告】
下一章,咱们进入系列的第五篇:NDK 构建系统:CMakeLists.txt 从入门到精通。我会给你讲透 CMakeLists.txt 的完整语法、常用配置、库文件链接技巧,重点教你怎么在项目里链接 RK3576 官方的原生库(librga、librknn_api),为后续的硬件加速、NPU 推理实战做好准备,再也不会被「undefined reference」报错搞崩!
我是黒漂技术佬,专注给小白搞懂 RK3576 安卓底层、JNI/NDK、嵌入式开发的保姆级教程,跟着我,保证你不迷路、不踩坑!
兄弟们,跟着本章跑通所有 demo 的,麻烦评论区扣个「JNI 引用类型搞定」!有啥问题、踩了啥坑,评论区直接留言,佬哥我挨个回复!点赞收藏关注不迷路,咱们下一章见!