【RK3576 安卓 JNI/NDK 系列 03】JNI 核心语法(上):数据类型映射与方法调用

目录

前言

一、先搞懂核心逻辑:为什么要有数据类型映射?

[二、JNI 数据类型映射全对照表,新手直接抄](#二、JNI 数据类型映射全对照表,新手直接抄)

[1. 基本类型映射(重点!天天要用)](#1. 基本类型映射(重点!天天要用))

[2. 引用类型映射(先了解,下一章细讲)](#2. 引用类型映射(先了解,下一章细讲))

[三、JNI 方法签名:新手 90% 的报错都来自这里](#三、JNI 方法签名:新手 90% 的报错都来自这里)

[1. 先搞懂:为什么要有方法签名?](#1. 先搞懂:为什么要有方法签名?)

[2. 方法签名的格式规则](#2. 方法签名的格式规则)

[3. 类型签名对照表,新手直接抄](#3. 类型签名对照表,新手直接抄)

[4. 举几个例子,一看就会](#4. 举几个例子,一看就会)

[5. 新手福音:不用手写!一键自动生成方法签名](#5. 新手福音:不用手写!一键自动生成方法签名)

[步骤 1:编译项目,生成 class 文件](#步骤 1:编译项目,生成 class 文件)

[步骤 2:打开 AS 的 Terminal 终端](#步骤 2:打开 AS 的 Terminal 终端)

[步骤 3:进入 class 文件的输出目录](#步骤 3:进入 class 文件的输出目录)

[步骤 4:执行 javap 命令,自动生成签名](#步骤 4:执行 javap 命令,自动生成签名)

命令参数说明:

[四、Java 调用 C++ 的标准流程:静态注册全步骤](#四、Java 调用 C++ 的标准流程:静态注册全步骤)

[步骤 1:Java 层编写 native 方法](#步骤 1:Java 层编写 native 方法)

[步骤 2:生成 C++ 对应的 JNI 函数(两种方式,新手推荐第二种)](#步骤 2:生成 C++ 对应的 JNI 函数(两种方式,新手推荐第二种))

[方式一:用 javah 命令生成头文件(传统方式)](#方式一:用 javah 命令生成头文件(传统方式))

[方式二:Android Studio 一键自动生成(新手首选,10 秒搞定)](#方式二:Android Studio 一键自动生成(新手首选,10 秒搞定))

[步骤 3:C++ 层实现 JNI 函数,编写业务逻辑](#步骤 3:C++ 层实现 JNI 函数,编写业务逻辑)

[步骤 4:配置 CMakeLists.txt,编译生成 so 库](#步骤 4:配置 CMakeLists.txt,编译生成 so 库)

[步骤 5:编译运行,看结果](#步骤 5:编译运行,看结果)

[五、新手踩坑急救站:本章 99% 的报错都在这里解决](#五、新手踩坑急救站:本章 99% 的报错都在这里解决)

[坑 1:运行报错 UnsatisfiedLinkError: No implementation found for xxx](#坑 1:运行报错 UnsatisfiedLinkError: No implementation found for xxx)

[坑 2:包名里有下划线,导致函数名写错,找不到方法](#坑 2:包名里有下划线,导致函数名写错,找不到方法)

[坑 3:方法签名写错,报NoSuchMethodError](#坑 3:方法签名写错,报NoSuchMethodError)

[坑 4:参数传递之后,数值不对,甚至溢出崩溃](#坑 4:参数传递之后,数值不对,甚至溢出崩溃)

[坑 5:JNI 函数里的日志不打印,或者看不到](#坑 5:JNI 函数里的日志不打印,或者看不到)

[本章总结 + 下章预告](#本章总结 + 下章预告)

【本章总结】

【下章预告】


前言

哈喽各位兄弟们,我是你们的黒漂技术佬!

上一章咱们搭好了全套环境,跑通了第一个 JNI HelloWorld 程序,后台直接炸了,一堆兄弟留言报喜:"佬哥,我成功在 RK3576 上跑通了!" 但同时也收到了 90% 兄弟的灵魂拷问:"佬哥,HelloWorld 我跑通了,但是我想给 C++ 传个 GPIO 编号、传个传感器的 I2C 地址,要么编译直接红一片,要么运行就崩,返回的值也不对,这是为啥啊?""为啥 Java 里的 int,到 C++ 里要写成 jint?我直接用 int 为啥有时候行有时候崩?""我想调用个重载方法,结果一直报 NoSuchMethodError,百度了半天说是方法签名写错了,这签名到底是个啥鬼东西啊?"

懂了懂了!这些问题,是所有 JNI 新手入门的第二道鬼门关 ------没搞懂 JNI 的核心交互规则。上一章的 HelloWorld 只是让你打通了流程,但是真正要写能用的代码,尤其是咱们 RK3576 底层开发要用到的硬件控制逻辑,必须把 JNI 的「数据类型映射」和「方法调用规则」彻底搞懂。

今天这一章,佬哥我用「大白话 + 对照表 + 实战 demo + 避坑指南」,把这些核心语法给你扒得明明白白,所有 demo 全是贴合咱们后续 RK3576 驱动开发的场景,学完就能直接用,再也不会被类型报错、找不到方法搞崩心态!


一、先搞懂核心逻辑:为什么要有数据类型映射?

上一章咱们把 JNI 类比成 Java 和 C/C++ 之间的「同声传译员」,那数据类型映射,就是翻译官手里的「中英单词对照表」

咱们先想一个问题:Java 里的int,和 C/C++ 里的int,是一个东西吗?很多新手会说:"不都是整数吗?肯定是一个东西啊!" 大错特错!这就是你写代码有时候行有时候崩的根源。

  • Java 是跨平台语言,它的基本类型长度是固定死的 ,不管是在 Windows 电脑、Linux 服务器,还是咱们 RK3576 的 arm64 安卓系统里,int永远是 4 字节 32 位,long永远是 8 字节 64 位,绝对不会变。
  • C/C++ 是和平台、编译器强绑定的,它的基本类型长度是不固定的:比如int在 32 位系统里是 4 字节,在某些 16 位嵌入式编译器里是 2 字节;long在 Windows 里是 4 字节,在 Linux 里是 8 字节。

你想啊,翻译官连单词对应的意思都搞不准,Java 说 "我要传一个 4 字节的 GPIO 编号 101",结果 C++ 当成 2 字节来读,那能不崩吗?

所以,JNI 官方专门定义了一套和 Java 类型一一对应、长度完全固定的类型体系 ,所有类型都以j开头,不管在什么平台、什么架构下,都能和 Java 的类型完美匹配,保证数据传递 100% 准确。这就是 JNI 数据类型映射的本质。

咱们 RK3576 是 arm64-v8a 架构,更要严格遵守这个规则,不然操作硬件寄存器、传递 64 位地址的时候,分分钟给你整出内存溢出、系统死机。


二、JNI 数据类型映射全对照表,新手直接抄

JNI 的类型分为两大类:基本类型引用类型,咱们分开讲,每一个都给你讲透用途,尤其是咱们 RK3576 开发里的使用场景。

1. 基本类型映射(重点!天天要用)

基本类型就是 Java 里的 8 种基础类型,在 JNI 里都有一一对应的j开头类型,长度完全固定,不会随平台变化。

我给你整理了一张对照表,连咱们 RK3576 开发里的具体用途都给你标好了,新手直接收藏,不用死记硬背,写代码的时候翻一下就行:

表格

Java 基本类型 JNI 对应类型 固定长度 大白话说明 RK3576 开发核心用途
boolean jboolean 1 字节 布尔值,对应 C++ 的 bool,只有 true (1)/false (0) 返回硬件操作的成功 / 失败状态
byte jbyte 1 字节 8 位有符号整数,对应 C++ 的 signed char 读写 I2C/SPI 设备的寄存器值、字节流数据
char jchar 2 字节 16 位无符号字符,对应 C++ 的 unsigned short 极少用,安卓开发基本不用管
short jshort 2 字节 16 位有符号整数,对应 C++ 的 short 传递 16 位的寄存器数据、PWM 占空比参数
int jint 4 字节 32 位有符号整数,对应 C++ 的 int 最常用!传递 GPIO 编号、I2C 总线号、硬件参数、运算数值
long jlong 8 字节 64 位有符号整数,对应 C++ 的 long long 传递 RK3576 的 64 位硬件内存地址、时间戳、大数值运算
float jfloat 4 字节 32 位单精度浮点数,对应 C++ 的 float 传递传感器的温湿度、电压电流等浮点数据
double jdouble 8 字节 64 位双精度浮点数,对应 C++ 的 double 高精度的算法运算、传感器数据处理
void void 无返回值,和 C++ 的 void 完全一致 无返回值的硬件初始化、GPIO 电平设置方法

划重点!新手必记的 3 个点:

  1. 最常用的就是jintjbooleanjbytejlong,咱们 RK3576 底层开发 90% 的场景都用这几个,别的可以用到再查。
  2. 绝对不要直接用 C++ 的原生类型代替 JNI 类型 !比如 Java 传了个long类型的 64 位地址,你在 C++ 里用int接收,直接就溢出了,操作硬件的时候轻则数据错误,重则系统直接死机。
  3. jboolean的值只有JNI_TRUE(1) 和JNI_FALSE(0),别直接用 C++ 的true/false,虽然大部分情况兼容,但偶尔会出玄学问题。

2. 引用类型映射(先了解,下一章细讲)

Java 里除了 8 种基本类型,剩下的全是引用类型,比如String、数组、自定义类、对象等等。JNI 里也给这些引用类型做了对应的映射,核心规则是:所有 Java 引用类型,在 JNI 里都对应jobject类型,或者它的子类

同样给大家整理了常用的引用类型对照表,先有个概念,下一章咱们会逐字逐句讲透字符串、数组、对象的具体操作:

表格

Java 引用类型 JNI 对应类型 大白话说明 RK3576 开发核心用途
String jstring Java 字符串类型,对应 JNI 的字符串类型 传递设备名称、日志信息、文件路径
int[] jintArray Java 基本类型数组,对应 JNI 的数组类型 传递批量的传感器数据、GPIO 配置参数
byte[] jbyteArray 字节数组,最常用的数组类型 传递摄像头图像数据、I2C 批量读写的字节流
Object jobject 所有 Java 对象的父类,任何 Java 对象都能用它接收 传递自定义的 Java 实体类,比如传感器数据对象
Class jclass Java 的 Class 类型,对应 JNI 的类类型 反射获取 Java 类的方法、属性
Throwable jthrowable Java 的异常类型,对应 JNI 的异常 在 C++ 层抛出 Java 能捕获的异常

划重点!引用类型和基本类型最大的区别:

  • 基本类型可以直接在 C++ 里用,比如jint a = 101;,直接赋值、直接运算,和 C++ 的 int 没区别。
  • 引用类型绝对不能直接在 C++ 里用 !比如你拿到一个jstring,不能直接当成char*来用,必须通过JNIEnv提供的方法转换之后才能用,不然直接崩溃。这个咱们下一章会细讲,这里先给你打个预防针。

三、JNI 方法签名:新手 90% 的报错都来自这里

搞懂了数据类型,咱们就来讲新手最头疼、报错最多的JNI 方法签名 。很多兄弟写代码,函数名写对了,类型也对应了,结果运行就报NoSuchMethodError,99% 的原因都是方法签名写错了。

1. 先搞懂:为什么要有方法签名?

咱们先想一个 Java 的基础语法:方法重载。比如咱们在 Java 的 MainActivity 里,写了两个名字完全一样的方法:

java

运行

复制代码
// 两个int相加
public int add(int a, int b) {
    return a + b;
}

// 两个float相加
public float add(float a, float b) {
    return a + b;
}

这两个方法,方法名都是add,只是参数类型、返回值类型不一样,Java 编译器能通过参数列表区分开这两个方法,这就是方法重载。

但是!JNI 是基于 C 语言规范设计的,C 语言里没有方法重载,函数名必须唯一,根本没法区分两个名字一样的方法。那怎么办?

JNI 就设计了方法签名 这个东西:用一串字符串,把一个方法的参数类型、参数个数、返回值类型全部描述出来,就算方法名一样,只要签名不一样,就能唯一区分开。

还是用翻译官的类比:方法名就是 "张三",重名的人很多,但是方法签名就是 "张三,男,30 岁,身份证号 xxx",能唯一确定一个人。

2. 方法签名的格式规则

方法签名的格式非常固定,记死这个公式就行:

plaintext

复制代码
(参数类型签名1参数类型签名2...)返回值类型签名

大白话拆解:

  1. 括号()里写所有参数的类型签名,有几个参数就写几个,没有空格、没有逗号 ,无参数就只写个空括号()
  2. 括号后面紧跟返回值的类型签名,没有任何分隔符
  3. 每个 Java 类型,都有唯一对应的签名字符串,和咱们上面讲的类型映射一一对应。

3. 类型签名对照表,新手直接抄

我给你整理了所有常用类型的签名对照表,不用死记硬背,写代码的时候翻一下就行:

表格

Java 类型 对应签名字符串 注意事项
boolean Z 别和 byte 的 B 搞混!
byte B 字节的首字母,好记
char C 字符的首字母
short S 短整型的首字母
int I 整型的首字母
long J 别和 long 的 L 搞混!L 是对象用的
float F 浮点型的首字母
double D 双精度的首字母
void V 无返回值专用
数组类型 [+ 元素类型签名 比如 int [] 的签名是[I,byte [] 的签名是[B,二维数组 int [][] 是[[I
类类型 L + 类全路径 +; 重点!全路径用/分隔,不是.,结尾必须加分号;!比如 String 的签名是Ljava/lang/String;,自定义类 com.heipiao.rk3576.jni.Student 的签名是Lcom/heipiao/rk3576/jni/Student;

4. 举几个例子,一看就会

光看规则太抽象,给大家举几个咱们开发中常用的例子,看完你就懂了:

表格

Java 方法声明 对应的方法签名 拆解说明
int add(int a, int b) (II)I 两个 int 参数,签名是 II,返回值 int 是 I,合起来就是 (II) I
boolean setGpioLevel(int gpioNum, boolean isHigh) (IZ)Z 第一个参数 int 是 I,第二个参数 boolean 是 Z,返回值 boolean 是 Z,合起来 (IZ) Z
String getDeviceName() ()Ljava/lang/String; 无参数,空括号,返回值 String 的签名是 Ljava/lang/String;,结尾分号不能丢
void initHardware() ()V 无参数,无返回值,就是 () V
float calcAvg(int[] data) ([I)F 参数是 int 数组,签名 [I,返回值 float 是 F,合起来 ([I) F

5. 新手福音:不用手写!一键自动生成方法签名

划重点!新手绝对不要手写方法签名 !写错一个字符,就会报NoSuchMethodError,找半天都找不到问题。

Java 官方给咱们提供了javap命令,能一键自动生成类里所有方法的签名,一步都不跳,跟着操作就行:

步骤 1:编译项目,生成 class 文件

在 Android Studio 里,点击菜单栏「Build」→「Make Project」,先把项目编译一遍,生成 Java 类对应的 class 文件。

步骤 2:打开 AS 的 Terminal 终端

点击 AS 底部的「Terminal」标签,打开终端窗口,默认路径就是你的项目根目录。

步骤 3:进入 class 文件的输出目录

在终端里输入以下命令,进入 class 文件所在的目录(替换成你自己的包名):

bash

运行

复制代码
# 进入app模块的class输出目录
cd app/build/intermediates/javac/debug/classes/
步骤 4:执行 javap 命令,自动生成签名

输入以下命令,就能生成对应类的所有方法签名(替换成你自己的完整包名 + 类名):

bash

运行

复制代码
# javap -s -p 完整包名.类名
javap -s -p com.heipiao.rk3576.jni.MainActivity
命令参数说明:
  • -s:输出方法签名,核心参数,必须加
  • -p:输出所有方法,包括 private 私有的,不加的话只会输出 public 方法

执行完之后,你就能在终端里看到类里所有方法的签名,直接复制粘贴就行,绝对不会写错!比如咱们的 add 方法,会输出:

plaintext

复制代码
  public int add(int, int);
    descriptor: (II)I

这里的descriptor:后面的(II)I,就是咱们要的方法签名,直接用就行,新手再也不用怕写错了!


四、Java 调用 C++ 的标准流程:静态注册全步骤

搞懂了类型和签名,咱们就来讲 Java 调用 C++ 的标准实现方式:静态注册。这也是咱们上一章 HelloWorld 用的方式,是新手入门必须掌握的核心流程,动态注册咱们后面进阶章节再讲。

静态注册的核心原理:通过固定的函数命名规则,把 Java 的 native 方法和 C++ 的函数一一绑定,Java 虚拟机加载 so 库的时候,会自动根据函数名找到对应的 C++ 函数,建立关联。

我给你整理了一步都不跳的标准流程,每一步都讲清楚,新手跟着走,绝对不会出错,而且所有步骤都贴合咱们 RK3576 的开发场景。

步骤 1:Java 层编写 native 方法

在你的 Java 类里(比如 MainActivity),用native关键字修饰你要声明的本地方法,这就相当于给翻译官下达了 "要翻译这句话" 的指令。

咱们结合 RK3576 的 GPIO 控制场景,写几个 native 方法:

java

运行

复制代码
package com.heipiao.rk3576.jni;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import com.heipiao.rk3576.jni.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // 加载so库,必须写在static代码块里,应用启动就加载
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        TextView tv = binding.sampleText;

        // 调用native方法,设置GPIO3_A5(编号101)为高电平
        boolean result = setGpioLevel(101, true);
        // 调用native方法,两个int相加
        int sum = add(100, 200);
        // 调用native方法,初始化硬件
        initHardware();

        // 把结果显示到屏幕上
        tv.setText("GPIO操作结果:" + result + "\n相加结果:" + sum);
    }

    // 1. native方法:设置GPIO电平,参数是GPIO编号、是否高电平,返回操作是否成功
    public native boolean setGpioLevel(int gpioNum, boolean isHigh);

    // 2. native方法:两个int相加,返回和
    public native int add(int a, int b);

    // 3. native方法:初始化硬件,无参数无返回值
    public native void initHardware();
}

步骤 2:生成 C++ 对应的 JNI 函数(两种方式,新手推荐第二种)

方式一:用 javah 命令生成头文件(传统方式)

javah 命令能自动根据 Java 的 native 方法,生成对应的 C/C++ 头文件,函数名、参数、返回值全给你生成好,绝对不会错。

操作步骤:

  1. 先编译项目,生成 class 文件(和上面生成方法签名的步骤一样)
  2. 打开 AS 的 Terminal,进入项目的app/src/main目录:

bash

运行

复制代码
cd app/src/main
  1. 执行 javah 命令,生成头文件:

bash

运行

复制代码
# javah -d 头文件输出目录 -classpath class文件目录 完整包名.类名
javah -d cpp -classpath ../../build/intermediates/javac/debug/classes/ com.heipiao.rk3576.jni.MainActivity
  1. 执行完之后,你会在cpp目录下看到生成的com_heipiao_rk3576_jni_MainActivity.h头文件,里面就是自动生成的 JNI 函数声明,直接复制到 cpp 文件里实现就行。
方式二:Android Studio 一键自动生成(新手首选,10 秒搞定)

现在的 Android Studio 已经支持一键生成 JNI 函数了,根本不用敲命令,新手直接用这个!

操作步骤:

  1. 写完 Java 的 native 方法之后,你会发现方法名是红色的,鼠标放上去会提示「Cannot resolve corresponding JNI function」
  2. 把光标放在红色的 native 方法名上,按下快捷键Alt+Enter(Windows)/Option+Enter(Mac)
  3. 在弹出的菜单里,选择「Create JNI function for xxx」
  4. AS 会自动在你的native-lib.cpp文件里,生成对应的 JNI 函数,函数名、参数、返回值全给你写好,绝对不会错!

步骤 3:C++ 层实现 JNI 函数,编写业务逻辑

自动生成函数之后,咱们就在 C++ 里写具体的业务逻辑,这里咱们给上面 3 个方法写实现,带详细注释:

打开app/src/main/cpp/native-lib.cpp,编写代码:

cpp

运行

复制代码
#include <jni.h>
#include <string>
// 引入安卓的log库,能在Logcat里看到C++的日志,和Java的Log.d一样
#include <android/log.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__)

// 1. 实现add方法:两个int相加,返回和
// 函数名是AS自动生成的,严格遵守静态注册的命名规则
extern "C" JNIEXPORT jint JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_add(JNIEnv *env, jobject thiz, jint a, jint b) {
    // 打印日志,能在Logcat里看到传入的参数
    LOGD("调用add方法,传入参数a=%d, b=%d", a, b);
    // 直接相加,返回结果,jint和int可以直接运算
    jint sum = a + b;
    LOGD("add方法计算结果:%d", sum);
    return sum;
}

// 2. 实现setGpioLevel方法:设置GPIO电平,返回操作是否成功
extern "C" JNIEXPORT jboolean JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_setGpioLevel(JNIEnv *env, jobject thiz, jint gpio_num,
                                                         jboolean is_high) {
    LOGD("调用setGpioLevel方法,GPIO编号=%d,设置电平=%s", gpio_num, is_high ? "高电平" : "低电平");

    // 这里咱们先写模拟逻辑,后面章节会写真正的GPIO硬件操作
    // 模拟:GPIO编号在0-200之间,操作成功,返回true,否则返回false
    if (gpio_num >= 0 && gpio_num <= 200) {
        LOGD("GPIO操作成功");
        return JNI_TRUE;
    } else {
        LOGE("GPIO编号非法,操作失败");
        return JNI_FALSE;
    }
}

// 3. 实现initHardware方法:初始化硬件,无参数无返回值
extern "C" JNIEXPORT void JNICALL
Java_com_heipiao_rk3576_jni_MainActivity_initHardware(JNIEnv *env, jobject thiz) {
    LOGD("调用initHardware方法,开始初始化RK3576硬件...");
    // 这里写硬件初始化逻辑,比如GPIO初始化、I2C总线初始化
    LOGD("RK3576硬件初始化完成!");
}

划重点!每个 JNI 函数的前两个参数是固定的,必须写,新手别乱改:

  • JNIEnv *env:JNI 环境指针,是咱们 JNI 开发的核心,所有 JNI 提供的方法,比如字符串转换、数组操作、对象调用,全是通过这个指针来调用的,相当于翻译官的工作手册。
  • jobject thiz:对应的 Java 对象,这里就是调用这个方法的 MainActivity 实例,相当于 this 指针。

步骤 4:配置 CMakeLists.txt,编译生成 so 库

咱们上一章已经配置好了 CMakeLists.txt,这里只要确保你的 cpp 文件已经添加到add_library里就行,不用改别的:

cmake

复制代码
cmake_minimum_required(VERSION 3.10.2)
project("rk3576_jni_helloworld")

add_library(
        native-lib
        SHARED
        native-lib.cpp) # 咱们的cpp文件,有多个的话都要加在这里

find_library(
        log-lib
        log)

target_link_libraries(
        native-lib
        ${log-lib})

步骤 5:编译运行,看结果

  1. 确保你的 RK3576 开发板已经通过 adb 连接到电脑
  2. 点击 AS 的 Run 按钮,编译运行 APK 到开发板上
  3. 你会在开发板的屏幕上看到对应的结果,同时打开 AS 的 Logcat,过滤标签Heipiao_RK3576_JNI,就能看到 C++ 里打印的日志,和咱们 Java 里的日志一模一样!

恭喜你!到这里,你已经完全掌握了 Java 调用 C++ 的标准流程,能自由传递基本类型参数、处理返回值,再也不会被类型报错搞崩了!


五、新手踩坑急救站:本章 99% 的报错都在这里解决

佬哥我把这一章里新手 100% 会踩的坑,全整理出来了,遇到报错直接来这里找解决方案,不用到处百度。

坑 1:运行报错 UnsatisfiedLinkError: No implementation found for xxx

99% 的原因

  1. 函数名写错了,静态注册的函数名必须严格遵守Java_包名_类名_方法名的规则,包名里的.必须换成_,错一个字母都不行。
  2. 函数前面没加extern "C",导致 C++ 编译器修改了函数名,Java 虚拟机找不到对应的函数。
  3. 忘记在static代码块里调用System.loadLibrary()加载 so 库,或者库名写错了。

解决方案

  1. 用 AS 的Alt+Enter自动生成函数,绝对不要手写函数名。
  2. 每个 JNI 函数前面必须加extern "C",别漏了。
  3. 检查System.loadLibrary()里的库名,和 CMakeLists.txt 里add_library的第一个参数完全一致。

坑 2:包名里有下划线,导致函数名写错,找不到方法

原因 :如果你的包名里有下划线,比如com.heipiao_rk3576.jni,那在 JNI 函数名里,下划线必须换成_1,不然 Java 会把下划线当成包名的分隔符,找不到对应的类。

正确写法 :包名com.heipiao_rk3576.jni对应的函数名前缀是Java_com_heipiao_1rk3576_jni_MainActivity_xxx

解决方案:新手尽量别在包名里加下划线,从根源上避免这个坑。

坑 3:方法签名写错,报NoSuchMethodError

原因 :99% 是手写签名写错了,尤其是类类型的签名,结尾的分号忘了,或者包名的分隔符用了.而不是/

解决方案 :绝对不要手写方法签名,用javap -s -p命令自动生成,复制粘贴绝对不会错。

坑 4:参数传递之后,数值不对,甚至溢出崩溃

原因 :Java 类型和 JNI 类型不匹配,比如 Java 传了个long类型的 64 位数值,你在 C++ 里用jint接收,直接溢出了。

解决方案 :严格遵守类型映射对照表,Java 的什么类型,JNI 里就用对应的j开头类型,绝对不要用 C++ 的原生类型代替。

坑 5:JNI 函数里的日志不打印,或者看不到

原因:忘记在 CMakeLists.txt 里链接 log 库,或者 Logcat 的标签过滤错了。

解决方案

  1. 检查 CMakeLists.txt 里的target_link_libraries里有没有链接${log-lib}
  2. 检查#define LOG_TAG的标签,和 Logcat 里过滤的标签完全一致。

本章总结 + 下章预告

【本章总结】

今天这一章,咱们彻底搞懂了 JNI 开发的核心基础,核心就 3 件事:

  1. 搞懂了 JNI 数据类型映射的本质,掌握了基本类型的对应规则,知道了为什么要用j开头的类型,再也不会传错参数。
  2. 搞懂了方法签名的作用和规则,学会了用javap命令自动生成签名,再也不会被NoSuchMethodError搞崩。
  3. 掌握了 Java 调用 C++ 的静态注册标准流程,能自己编写 native 方法,实现对应的 C++ 逻辑,传递参数、处理返回值,为后续的 RK3576 硬件操作打下了基础。

【下章预告】

下一章,咱们进入 JNI 核心语法的下半部分:JNI 核心语法(下):字符串、数组与对象操作。我会给你讲透 JNI 里最常用的字符串转换、数组读写、Java 对象操作,带你写多个贴合 RK3576 场景的实战 demo,比如传递传感器的批量数据、图像字节流、自定义的设备数据对象,学完你就能处理 90% 的 JNI 开发场景!


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

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

相关推荐
白雪落青衣14 分钟前
buuoj course 1详细解析
android
恋猫de小郭31 分钟前
Android 发布全新性能分析器,实用性和性能大升级
android·前端·flutter
Kapaseker41 分钟前
为什么 Java 的数组需要 new 出来
android·java·kotlin
黄林晴1 小时前
颠覆开发!Google AI Studio 一句话生成原生 Android App
android·google io
恋猫de小郭1 小时前
Flutter 3.44 发布啦,超级大版本更新!!!
android·flutter·ios
zb200641201 小时前
Laravel10.x重磅升级:新特性全解析
android
2601_957418801 小时前
深入解析Android相机有线连接:PTP与MTP协议栈实现原理与实践
android·数码相机·智能手机
努力努力再努力wz2 小时前
【QT入门系列】QWidget 六大常用属性详解:windowOpacity、cursor、font、focus、toolTip 与 styleSheet
android·开发语言·数据结构·c++·qt·mysql·算法
撩得Android一次心动2 小时前
C语言基础笔记3【个人用】
android·c语言·开发语言·笔记
小离a_a2 小时前
uniapp小程序封装圆环显示比例数据
android·小程序·uni-app