【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 基础语法搞定」!有啥问题、踩了啥坑,评论区直接留言,佬哥我挨个回复!点赞收藏关注不迷路,咱们下一章见!

相关推荐
XerCis2 小时前
安卓手机搭建Samba服务器SMB
android·服务器·智能手机
studyForMokey2 小时前
【Android面试】Context专题
android·面试·职场和发展
三少爷的鞋14 小时前
从 MVVM 到 MVI:为什么说 MVVM 的 UI 状态像“网”,而 MVI 像“一条线”?
android
蜡台14 小时前
Flutter 安装配置
android·java·flutter·环境变量
阿乐艾官14 小时前
【HBase列式存储数据库】
android·数据库·hbase
yoyo_zzm16 小时前
MySQL的索引
android·数据库·mysql
Okailon16 小时前
PHP面向对象模块 jc-simple-footer 的技术详解
android·php·开源软件·家谱软件
llxxyy卢17 小时前
polar-web部分中等题目
android·前端·sql·web安全
zJianFlys18 小时前
Android16(API36)在获取WiFi信息时SSID为<unknown ssid>
android