【RK3576 安卓 JNI/NDK 系列 04】JNI 核心语法(下):字符串、数组与对象操作

目录

前言

[先打预防针: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 的StringUTF-16 编码,每个字符占 2 字节,中文、英文都是 2 字节;
  • C++ 的char*UTF-8 编码,英文占 1 字节,中文占 3 字节,是 Linux 系统、C/C++ 开发的通用编码。

你直接把 UTF-16 的 jstring 当成 UTF-8 的 char * 用,就像把中文书直接给只会英文的人看,全是乱码,不崩才怪。

JNI 专门提供了一套方法,帮你完成jstringchar*的互转,同时处理编码问题,咱们只需要按步骤调用就行。

2. 核心操作 1:Java→C++(jstring 转 char*)

这是最常用的场景:Java 层把设备路径、文件名传给 C++ 层,C++ 层用来打开设备、读写文件。

标准步骤(一步都不能少)
  1. GetStringUTFChars获取 UTF-8 编码的 char * 指针;
  2. 校验指针是否为空,避免空指针崩溃;
  3. 使用 char * 完成你的业务逻辑;
  4. 必须用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 个坑

  1. 不释放 GetStringUTFChars 获取的指针:内存泄漏的重灾区,记住:有 Get 必有 Release,成对出现!
  2. 不做空指针校验:Java 传 null 进来,GetStringUTFChars 会返回 NULL,直接用会触发空指针崩溃。
  3. 把 jstring 直接当 char * 用:编码不匹配,要么乱码,要么直接崩溃,必须用 JNI 方法转换。
  4. ReleaseStringUTFChars 之后还继续使用 char * 指针:释放之后指针就失效了,再用就是野指针,必崩。
  5. 中文乱码:确保 C++ 的 char * 是 UTF-8 编码,别用 GBK 编码,不然 Java 层拿到的全是问号。

二、JNI 数组操作:全类型数组读写详解

数组是咱们 RK3576 底层开发里用得最多的引用类型,没有之一:

  • 传感器批量采样数据:int 数组、float 数组;
  • I2C/SPI 读写的字节流:byte 数组;
  • 摄像头、RGA 图像处理的帧数据:byte 数组、int 数组;
  • GPIO 批量配置参数:int 数组。

JNI 里的数组分为基本类型数组 (jintArray、jbyteArray 等)和对象数组(jobjectArray),咱们先讲 90% 场景都会用到的基本类型数组,对象数组放在后面对象操作里讲。

1. 数组操作的核心规则

  1. 基本类型数组不能直接用 C++ 的指针访问,必须通过 JNI 提供的方法获取数组元素;
  2. 获取数组元素有两种模式:拷贝模式 (JNI 把 Java 数组拷贝到 C++ 堆里)和直接指针模式(直接返回 Java 数组的内存地址),不用关心底层用了哪种模式,按步骤调用就行;
  3. 有 Get 必有 Release,获取的数组指针必须手动释放,不然内存泄漏;
  4. 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 个坑

  1. 不释放数组指针:和字符串一样,有 Get 必有 Release,成对出现,不然内存泄漏;
  2. 数组越界访问:JNI 不会帮你检查数组长度,循环的时候一定要用 GetArrayLength 获取的长度,别硬编码,越界直接崩溃;
  3. 空指针校验缺失:Java 传 null 数组进来,Get 方法会返回 NULL,直接用必崩;
  4. Release 之后还使用指针:释放之后指针就失效了,再用就是野指针,大概率触发 Native 崩溃,很难排查。

三、JNI 对象操作:jobject 属性访问与方法调用

咱们做实际开发的时候,不会只传单个数值或者数组,更多时候会用自定义的 Java 实体类封装数据,比如:

  • 传感器数据类:封装温度、湿度、时间戳、设备 ID;
  • 设备信息类:封装设备名称、硬件版本、固件版本、状态;
  • 回调接口:C++ 层收到硬件中断,调用 Java 层的回调方法更新 UI。

这些场景,都需要 JNI 操作 Java 对象(jobject),核心就是两个操作:访问对象的属性调用对象的方法

1. 对象操作的核心原理

Java 的对象,本质上是「属性 + 方法」的集合,JNI 要操作对象,必须先拿到两个东西:

  1. jclass:对象对应的 Java 类的 Class 对象,相当于对象的 "设计图纸";
  2. FieldID/MethodID:属性 / 方法的唯一标识,相当于图纸上的 "零件编号",通过这个编号才能找到对应的属性 / 方法。

划重点!FieldID 和 MethodID 是可以缓存的,不要每次调用 JNI 函数都去获取,不然会严重影响性能,尤其是高频调用的场景(比如每帧图像处理、传感器高频采样),这个是新手最容易忽略的性能优化点。

2. 核心操作 1:C++ 访问 Java 对象的属性

标准步骤
  1. GetObjectClass获取对象对应的 jclass;
  2. GetFieldID获取属性的 FieldID,需要传入属性名、属性的签名(上一章讲的类型签名);
  3. Get<Type>Field获取属性的值,传入对象和 FieldID;
  4. 如果要修改属性的值,用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、触发业务逻辑。

标准步骤
  1. GetObjectClass获取对象的 jclass;
  2. GetMethodID获取方法的 MethodID,需要传入方法名、方法签名(上一章讲的方法签名,必须用 javap 自动生成!);
  3. 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 个坑

  1. 属性名 / 方法名写错,或者签名写错 :99% 的NoSuchFieldError/NoSuchMethodError都是这个原因,绝对不要手写签名,用 javap 命令自动生成!
  2. 不缓存 FieldID/MethodID:每次调用都去获取 ID,会严重影响性能,尤其是高频调用的场景,一定要缓存起来。
  3. 局部引用全局使用 :方法里拿到的 jobject、jclass 都是局部引用,方法结束就会失效,要全局使用必须用NewGlobalRef创建全局引用,不然会被 GC 回收,导致野指针崩溃。
  4. 全局引用不释放 :用NewGlobalRef创建的全局引用,必须用DeleteGlobalRef释放,不然永远不会被 GC 回收,造成严重的内存泄漏。
  5. 在子线程里调用 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,野指针崩溃

原因:大概率是以下几种情况:

  1. 访问了已经 Release 的字符串 / 数组指针;
  2. 数组越界访问;
  3. 空指针没有校验,直接使用 NULL 指针。解决方案:所有 Get 方法返回的指针都做空指针校验,有 Get 必有 Release,数组循环严格控制长度。

报错 4:内存泄漏,APP 跑久了 OOM 崩溃

原因

  1. GetStringUTFChars、GetArrayElements 获取的指针没有 Release;
  2. 全局引用没有 DeleteGlobalRef 释放;
  3. 循环里频繁创建局部引用,没有手动 DeleteLocalRef。解决方案 :所有引用都成对处理,有申请必有释放,循环里的局部引用用完就用env->DeleteLocalRef()释放。

本章总结 + 下章预告

【本章总结】

今天这一章,咱们彻底啃完了 JNI 引用类型的核心操作,核心就 4 件事:

  1. 搞懂了引用类型和基本类型的核心区别,记住了「有 Get 必有 Release」的铁律,从根源上避免内存泄漏;
  2. 掌握了 jstring 的完整操作,能自由实现 Java 和 C++ 之间的字符串互转,传设备路径、设备信息再也不会崩;
  3. 掌握了基本类型数组的读写操作,能处理传感器批量数据、图像字节流,为后续的硬件操作、RGA 图像处理打下了基础;
  4. 掌握了 Java 对象的属性访问和方法调用,能实现 C++ 到 Java 的回调,完成硬件数据的实时 UI 更新,搞定了实际开发的核心架构。

【下章预告】

下一章,咱们进入系列的第五篇:NDK 构建系统:CMakeLists.txt 从入门到精通。我会给你讲透 CMakeLists.txt 的完整语法、常用配置、库文件链接技巧,重点教你怎么在项目里链接 RK3576 官方的原生库(librga、librknn_api),为后续的硬件加速、NPU 推理实战做好准备,再也不会被「undefined reference」报错搞崩!


我是黒漂技术佬,专注给小白搞懂 RK3576 安卓底层、JNI/NDK、嵌入式开发的保姆级教程,跟着我,保证你不迷路、不踩坑!

兄弟们,跟着本章跑通所有 demo 的,麻烦评论区扣个「JNI 引用类型搞定」!有啥问题、踩了啥坑,评论区直接留言,佬哥我挨个回复!点赞收藏关注不迷路,咱们下一章见!

相关推荐
2501_915909061 小时前
不用越狱就看不到 iOS App 内部文件?使用 Keymob 查看和导出应用数据目录
android·ios·小程序·https·uni-app·iphone·webview
llxxyy卢2 小时前
web部分中等题目
android·前端
轩情吖2 小时前
MySQL之事务管理
android·后端·mysql·adb·事务·隔离性·原子性
weiyvyy2 小时前
接口开发的完整流程:从需求到验证
驱动开发·嵌入式硬件·硬件架构·硬件工程
万物得其道者成2 小时前
uni-app Android 离线打包:多环境(prod/dev)配置
android·opencv·uni-app
符哥20082 小时前
Firebase 官方提供的Quick Start-Android 库的功能集讲解
android
koeda3 小时前
android17系统兼容
android·安卓
进击的cc3 小时前
面试官:Handler 没消息时为啥不卡死?带你从源码到底层内核彻底整明白!
android·面试
Yang-Never4 小时前
OpenGL ES ->YUV图像基础知识
android·java·开发语言·kotlin·android studio