在RK3576这颗芯片上做安卓开发,绕不开JNI。RK3576是瑞芯微面向边缘计算、智能座舱、工业HMI等场景的中高端SoC,4个Cortex-A72大核加4个Cortex-A53小核,还集成了一颗6 TOPS算力的NPU,跑Android 15很轻松。但越是这种性能向的板子,越需要把图像处理、音视频编解码、算法加速这些重活下放到C/C++层,再通过JNI接口暴露给Java层调用。这篇是这个系列的第03篇,前两篇已经帮你把NDK环境、CMake工程在RK3576上搭好,这一篇聚焦JNI核心语法里最基础也最绕的两个部分:数据类型映射和方法调用。适合刚把NDK工程跑起来、正准备写第一个正经native接口的开发者,不整虚的,全部围绕实战来说。
1. 为什么在RK3576上做安卓开发要死磕JNI
1.1 平台现实:Java层并不是万能的
RK3576这颗SoC在性能上的定位,决定了它的项目大概率不会是纯Java应用。芯片内置的NPU、RGA图形加速模块、MPP媒体处理平台,在官方SDK里都是以C/C++库的形式提供的。比如你要在Java层做YUV到RGBA的格式转换,利用RGA硬件加速,直接调Java API不现实,必须通过NDK把RGA的C接口包一层。再比如H.264/H.265硬解的码流数据,经常是放在硬件分配的buffer里,Java层拿不到直接指针,也得走Native。
这不是说Java不行,而是场景不对。UI逻辑、业务状态机、网络协议栈这些,Java/Kotlin开发效率高、生态好,没理由在C++里重造一遍。但那种逐帧处理、内存拷贝敏感、实时性要求高的任务,一旦走Java层,GC暂停、内存拷贝、JIT预热这类开销就非常难受。JNI出现的意义就是让这两种语言各干各擅长的活,中间通过一套明确定义的ABI来通信。
这个权衡在整个安卓生态里是普遍的,但在RK3576上尤其明显。因为它同时具备NPU、GPU、多媒体协处理、多屏显示等硬件能力,一个典型产品可能要同时调好几个Native库。比如NPU做人脸识别、MPP做视频推流、RGA做图像变换,然后这些Native能力都要汇入一个安卓App的统一控制逻辑里。JNI层就是所有这些数据流通的关口,这里写不好,整个系统都会跟着遭殃。
1.2 JNI调用链拆解
先弄清楚一次JNI调用从Java到C/C++到底发生了什么。Java层声明一个native方法后,编译器会生成一个对应的符号引用。当Java代码第一次调用这个方法时,虚拟机在已经加载的native库里查找对应的导出函数,找到后就为这个调用建立一个桩(stub),后续调用直接走这个桩,不需要再做动态查找。查找规则是固定的:Java_包名_类名_方法名,包名里的点号替换为下划线,方法名原样保留,如果是重载方法还会在末尾追加参数签名。
这个规则看起来简单,但里面有好几个容易出错的地方。首先是包名:如果类在com.rk3576.jnidemo包下,那函数名必须是Java_com_rk3576_jnidemo_NativeBridge_nativeAdd,不能多一个下划线也不能少。其次是区分实例方法和静态方法:实例方法第一个参数是JNIEnv*和jobject(指代this对象),静态方法第一个参数是JNIEnv*和jclass(指代Class对象),这两个很容易搞混,尤其是同一份C代码给两个类用的时候。
还有Java层加载库的时机:System.loadLibrary("jnidemo")在static块里执行,它找到的是libjnidemo.so。如果找不到,或者so里的导出符号和Java声明对不上,运行时就会抛UnsatisfiedLinkError。这种错误往往不在第一调用点暴露,而是在你真正调用那个native方法时才炸出来,所以排查起来容易摸不着头脑。
1.3 快速准备一套RK3576可跑的NDK工程
不是所有人都是照着系列前两篇一路跟过来的,我把环境准备要点再压缩一遍。开发机建议Ubuntu 22.04,NDK用r25c或r26以上都可以,CMake 3.22及以上。在Android Studio里新建工程时勾选Native C++,或者给已有工程添加CMake支持都行。
一个关键点是ABI。RK3576是64位ARMv8架构,Android系统是64位的,所以JNI库只需要编译arm64-v8a。在build.gradle里配置:
groovy复制android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++17"
}
}
ndk {
abiFilters 'arm64-v8a'
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
注意这里设置abiFilters后,APK里只会包含64位so,体积小,也不会出现32位/64位不匹配的坑。如果你的板子固件是32位系统,就要编译armeabi-v7a,但RK3576本身是64位SoC,Android 15系统也基本以64位为主,所以arm64-v8a是默认选择。
CMakeLists.txt初始版本这样写就够:
cmake复制cmake_minimum_required(VERSION 3.22)
project(jnidemo)
add_library(jnidemo SHARED native-lib.cpp)
find_library(log-lib log)
target_link_libraries(jnidemo ${log-lib})
这种工程结构跑起来后,就可以开始谈今天的正题了。数据映射和方法调用是所有JNI代码的地基,后面的线程、引用、异常处理全是在这上面长出来的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据类型映射:这是JNI的第一道门槛
2.1 基本类型映射:一张表背下来
JNI里最基本的数据类型映射,其实只涉及8个基本类型加void。我把完整映射列表放在下面,这张表建议打印出来贴屏幕旁边,后面前写JNI函数签名、写C/C++函数参数,几乎所有环节都要到这里查。
| Java类型 | JNI类型 | C/C++类型 | 字节宽度 | JNI签名 |
|---|---|---|---|---|
| boolean | jboolean | unsigned char | 1字节 | Z |
| byte | jbyte | signed char | 1字节 | B |
| char | jchar | unsigned short | 2字节 | C |
| short | jshort | short | 2字节 | S |
| int | jint | int | 4字节 | I |
| long | jlong | long long | 8字节 | J |
| float | jfloat | float | 4字节 | F |
| double | jdouble | double | 8字节 | D |
| void | void | void | 无 | V |
第一个要背下来的坑,是Java的boolean映射到C层不是C++的bool,而是unsigned char,底层用0表示false、非0表示true。你在C++里对jboolean做if (b)判断没问题,但如果直接把它强转成bool再参与位运算,就可能因为字节宽度不同而出问题。我见过有人把jboolean数组当bool数组用,结果因为jboolean是unsigned char一字节,bool在某些编译器和平台上也是一字节,看起来没问题,但语义是错的,一旦遇到非0非1的值就翻车。
第二个坑是long。Java的long是64位,映射到C++的long long,不是long。在64位Linux上long本身是64位,但在Windows上long是32位,而安卓NDK的编译目标基本都是Linux,所以long在arm64上是64位。不过为了代码可移植性,还是建议统一用jlong和int64_t,不要依赖平台long宽度。
其他几个相对简单:int就是C的int,char特别注意,JNI里对应的是jchar,这不是C语言里的char,而是UTF-16编码单元,长度为16位。这种16位无符号的概念和Java char一致,但和C程序员习惯的ASCII字符完全不同。
2.2 字符串处理:jstring不是char*
字符串是JNI数据类型映射里
