目录
[二、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 个点:
- 最常用的就是
jint、jboolean、jbyte、jlong,咱们 RK3576 底层开发 90% 的场景都用这几个,别的可以用到再查。 - 绝对不要直接用 C++ 的原生类型代替 JNI 类型 !比如 Java 传了个
long类型的 64 位地址,你在 C++ 里用int接收,直接就溢出了,操作硬件的时候轻则数据错误,重则系统直接死机。 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...)返回值类型签名
大白话拆解:
- 括号
()里写所有参数的类型签名,有几个参数就写几个,没有空格、没有逗号 ,无参数就只写个空括号()。 - 括号后面紧跟返回值的类型签名,没有任何分隔符。
- 每个 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++ 头文件,函数名、参数、返回值全给你生成好,绝对不会错。
操作步骤:
- 先编译项目,生成 class 文件(和上面生成方法签名的步骤一样)
- 打开 AS 的 Terminal,进入项目的
app/src/main目录:
bash
运行
cd app/src/main
- 执行 javah 命令,生成头文件:
bash
运行
# javah -d 头文件输出目录 -classpath class文件目录 完整包名.类名
javah -d cpp -classpath ../../build/intermediates/javac/debug/classes/ com.heipiao.rk3576.jni.MainActivity
- 执行完之后,你会在
cpp目录下看到生成的com_heipiao_rk3576_jni_MainActivity.h头文件,里面就是自动生成的 JNI 函数声明,直接复制到 cpp 文件里实现就行。
方式二:Android Studio 一键自动生成(新手首选,10 秒搞定)
现在的 Android Studio 已经支持一键生成 JNI 函数了,根本不用敲命令,新手直接用这个!
操作步骤:
- 写完 Java 的 native 方法之后,你会发现方法名是红色的,鼠标放上去会提示「Cannot resolve corresponding JNI function」
- 把光标放在红色的 native 方法名上,按下快捷键
Alt+Enter(Windows)/Option+Enter(Mac) - 在弹出的菜单里,选择「Create JNI function for xxx」
- 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:编译运行,看结果
- 确保你的 RK3576 开发板已经通过 adb 连接到电脑
- 点击 AS 的 Run 按钮,编译运行 APK 到开发板上
- 你会在开发板的屏幕上看到对应的结果,同时打开 AS 的 Logcat,过滤标签
Heipiao_RK3576_JNI,就能看到 C++ 里打印的日志,和咱们 Java 里的日志一模一样!
恭喜你!到这里,你已经完全掌握了 Java 调用 C++ 的标准流程,能自由传递基本类型参数、处理返回值,再也不会被类型报错搞崩了!
五、新手踩坑急救站:本章 99% 的报错都在这里解决
佬哥我把这一章里新手 100% 会踩的坑,全整理出来了,遇到报错直接来这里找解决方案,不用到处百度。
坑 1:运行报错 UnsatisfiedLinkError: No implementation found for xxx
99% 的原因:
- 函数名写错了,静态注册的函数名必须严格遵守
Java_包名_类名_方法名的规则,包名里的.必须换成_,错一个字母都不行。 - 函数前面没加
extern "C",导致 C++ 编译器修改了函数名,Java 虚拟机找不到对应的函数。 - 忘记在
static代码块里调用System.loadLibrary()加载 so 库,或者库名写错了。
解决方案:
- 用 AS 的
Alt+Enter自动生成函数,绝对不要手写函数名。 - 每个 JNI 函数前面必须加
extern "C",别漏了。 - 检查
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 的标签过滤错了。
解决方案:
- 检查 CMakeLists.txt 里的
target_link_libraries里有没有链接${log-lib}。 - 检查
#define LOG_TAG的标签,和 Logcat 里过滤的标签完全一致。
本章总结 + 下章预告
【本章总结】
今天这一章,咱们彻底搞懂了 JNI 开发的核心基础,核心就 3 件事:
- 搞懂了 JNI 数据类型映射的本质,掌握了基本类型的对应规则,知道了为什么要用
j开头的类型,再也不会传错参数。 - 搞懂了方法签名的作用和规则,学会了用
javap命令自动生成签名,再也不会被NoSuchMethodError搞崩。 - 掌握了 Java 调用 C++ 的静态注册标准流程,能自己编写 native 方法,实现对应的 C++ 逻辑,传递参数、处理返回值,为后续的 RK3576 硬件操作打下了基础。
【下章预告】
下一章,咱们进入 JNI 核心语法的下半部分:JNI 核心语法(下):字符串、数组与对象操作。我会给你讲透 JNI 里最常用的字符串转换、数组读写、Java 对象操作,带你写多个贴合 RK3576 场景的实战 demo,比如传递传感器的批量数据、图像字节流、自定义的设备数据对象,学完你就能处理 90% 的 JNI 开发场景!
我是黒漂技术佬,专注给小白搞懂 RK3576 安卓底层、JNI/NDK、嵌入式开发的保姆级教程,跟着我,保证你不迷路、不踩坑!
兄弟们,跟着本章跑通 demo 的,麻烦评论区扣个「JNI 基础语法搞定」!有啥问题、踩了啥坑,评论区直接留言,佬哥我挨个回复!点赞收藏关注不迷路,咱们下一章见!