第1章 JNI基础:Java与C/C++的桥梁
各位同学,欢迎来到这门课的第一章。
做Android底层开发,JNI是绕不开的一道坎。说白了,JNI就是Java和C/C++之间的翻译官。Java那边说「我要调一个本地方法」,C/C++这边就得听懂并执行。我刚开始接触JNI时,觉得这东西不就是个接口嘛,后来踩了不少坑才明白——细节决定成败。
1.1 为什么需要JNI?
Android应用层用Java/Kotlin写,但底层库、驱动、算法优化,很多都是C/C++的天下。比如音视频编解码、OpenGL渲染、加密算法,这些场景用Java写性能跟不上,或者根本没法直接操作硬件。
JNI就是用来打通这个壁垒的。它让Java代码能调用C/C++函数,反过来C/C++也能回调Java方法。嗯,这里要注意——JNI不是Android独有的,它是Java标准的一部分。Android只是把它用到了极致。
核心理解:JNI是一套协议,定义了Java虚拟机如何与本地代码交互。没有它,Java和C/C++就是两个世界的人。
1.2 JNI函数命名规则
这个规则,我当年第一次看时觉得挺繁琐,但用久了发现——它其实很机械,完全可预测。
JNI函数的命名格式如下:
Java_包名_类名_方法名
举个例子。假设你在Java中有一个类:
package com.example.ndk;
public class NativeLib {
public static native String stringFromJNI();
}
那么对应的C/C++函数名就是:
Java_com_example_ndk_NativeLib_stringFromJNI
规则很简单:
- 以
Java_开头 - 包名中的点
.替换为下划线_ - 类名和方法名直接拼接
- 如果方法名中有特殊字符(比如Unicode),需要用编码转义——不过这种情况很少见
我曾经踩过的坑:包名或类名里带了数字或下划线,结果生成的函数名和预期对不上。比如包名 com.my_app,下划线会被当作分隔符处理,导致JNI找不到函数。解决办法是避免在包名中使用下划线,或者用 javah 工具自动生成头文件,别手写。
1.3 JNI数据类型映射
Java和C/C++的数据类型不一样,JNI定义了一套映射规则。我习惯把它分成两类:基本类型和引用类型。
基本类型映射
| Java类型 | JNI类型 | C/C++类型 | 说明 |
|---|---|---|---|
| boolean | jboolean | unsigned char | 0表示false,非0表示true |
| byte | jbyte | signed char | 8位有符号 |
| char | jchar | unsigned short | 16位Unicode |
| short | jshort | short | 16位有符号 |
| int | jint | int | 32位有符号 |
| long | jlong | long long | 64位有符号 |
| float | jfloat | float | 32位浮点 |
| double | jdouble | double | 64位浮点 |
你看,基本类型的映射很直接。但引用类型就复杂一些了。
引用类型映射
| Java类型 | JNI类型 | 说明 |
|---|---|---|
| String | jstring | Java字符串,需要转换才能用 |
| Object[] | jobjectArray | 对象数组 |
| byte[] | jbyteArray | 字节数组 |
| Class | jclass | Java类对象 |
| Throwable | jthrowable | 异常对象 |
引用类型在C/C++中都是不透明指针。你不能直接操作它们的内容,必须通过JNI提供的函数来读写。比如 jstring 要用 GetStringUTFChars 才能拿到C字符串。
个人建议:处理字符串时,记得调用 ReleaseStringUTFChars 释放内存。我见过不少新手只拿不还,导致内存泄漏。JNI的内存管理,后面章节会详细讲。
1.4 javah工具的使用
javah是JDK自带的一个工具,用来从Java类生成JNI头文件。有了它,你就不用自己手写函数声明了——省心又防错。
用法很简单。假设你的Java文件是 NativeLib.java:
// 先编译Java文件
javac com/example/ndk/NativeLib.java
// 然后运行javah
javah -jni com.example.ndk.NativeLib
执行后,会生成一个 com_example_ndk_NativeLib.h 文件。内容大概是这样:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_ndk_NativeLib */
#ifndef _Included_com_example_ndk_NativeLib
#define _Included_com_example_ndk_NativeLib
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_ndk_NativeLib
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_ndk_NativeLib_stringFromJNI
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
注意看,函数签名里多了两个参数:JNIEnv* 和 jclass(静态方法)或 jobject(实例方法)。这两个参数是JNI框架自动传入的,你不用自己传。
重要提醒:javah在JDK 10以后被标记为废弃,推荐用 javac -h 替代。比如:
javac -h . com/example/ndk/NativeLib.java
效果一样,但更现代。我个人现在都用 javac -h,省得每次还要单独敲javah。
1.5 简单JNI示例
光说不练假把式。我们来写一个完整的JNI示例,从Java到C再到编译运行。
第一步:写Java类
// NativeLib.java
package com.example.ndk;
public class NativeLib {
static {
System.loadLibrary("native-lib");
}
public static native String stringFromJNI();
public static native int add(int a, int b);
}
第二步:生成头文件
javac -h . com/example/ndk/NativeLib.java
第三步:实现C/C++代码
// native-lib.cpp
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndk_NativeLib_stringFromJNI(
JNIEnv* env,
jclass /* clazz */) {
std::string hello = "Hello from C++!";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT jint JNICALL
Java_com_example_ndk_NativeLib_add(
JNIEnv* env,
jclass /* clazz */,
jint a,
jint b) {
return a + b;
}
第四步:编写CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project("native-lib")
add_library(
native-lib
SHARED
native-lib.cpp
)
find_library(log-lib log)
target_link_libraries(native-lib ${log-lib})
第五步:在Android中调用
// MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String msg = NativeLib.stringFromJNI();
int sum = NativeLib.add(3, 5);
Log.d("JNI", "msg: " + msg + ", sum: " + sum);
}
}
运行后,你会看到日志输出:msg: Hello from C++!, sum: 8。嗯,就是这么简单。
避坑指南:我曾经在 System.loadLibrary 时写错了库名,结果运行时直接崩溃。库名要和CMake中 add_library 指定的名字一致,而且不要加 lib 前缀和 .so 后缀。
1.6 知识体系总览
为了让你对本章内容有个整体印象,我画了一张图。它展示了JNI在Java和C/C++之间的位置,以及本章涉及的核心知识点。
这张图把本章的知识点串起来了。Java层声明native方法,通过JNI桥梁找到C/C++层的实现。函数命名规则和数据类型映射,就是这座桥的「交通规则」。javah工具帮你自动生成路标,而示例代码则是一次完整的通车演练。
好了,第一章就到这里。JNI的基础概念、命名规则、数据类型、javah用法,以及一个完整的示例,你都过了一遍。这些东西看着简单,但后面每一章都会用到它们。尤其是数据类型映射和函数命名,记牢了,后面写代码会顺手很多。