第一章:Native方法声明与实现
各位同学,今天我们来聊聊JNI最基础、也最核心的部分——Native方法的声明与实现。说实话,我刚接触NDK那会儿,被JNI的函数命名规则折磨得不轻。但搞懂了背后的设计思路,你会发现它其实很规整。
1.1 Java中的native关键字
在Java里,native关键字告诉虚拟机:这个方法我不在Java层实现,而是交给C/C++去处理。写法很简单:
public class NativeBridge {
// 声明一个native方法
public native String stringFromJNI();
// 带参数的方法
public native int add(int a, int b);
// 静态native方法
public static native String getBuildType();
}
注意,native方法只有声明,没有方法体。它就像一份合同,Java这边签了字,具体干活的是C/C++那边。
关键点:native方法可以是实例方法,也可以是静态方法。返回值可以是基本类型、String、数组,甚至是自定义的Java对象。
1.2 C/C++中JNI函数实现
Java声明完了,C/C++这边怎么接?JNI有一套严格的命名规则:
// 格式:Java_包名_类名_方法名
// 包名中的点(.)要换成下划线(_)
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_NativeBridge_stringFromJNI(
JNIEnv* env,
jobject thiz) {
// 实现代码
return env->NewStringUTF("Hello from C++!");
}
extern "C" JNIEXPORT jint JNICALL
Java_com_example_NativeBridge_add(
JNIEnv* env,
jobject thiz,
jint a,
jint b) {
return a + b;
}
这里有几个细节我得提醒你:
extern "C"防止C++名字修饰,保证JNI能找到函数JNIEXPORT和JNICALL是平台相关的宏,保证函数导出和调用约定正确- 函数名必须和Java层完全匹配,大小写敏感
我曾经因为包名写错一个字母,导致运行时一直报UnsatisfiedLinkError。排查了半天才发现是包名大小写问题。嗯,JNI函数名是区分大小写的,千万别马虎。
1.3 JNIEnv与jobject的使用
每个JNI函数的前两个参数是固定的:JNIEnv* 和 jobject(或 jclass)。
JNIEnv 是JNI环境的接口指针。说白了,它就是C/C++访问Java世界的桥梁。你想调用Java的方法、访问Java的字段、创建Java对象,都得通过它。
jobject 代表调用这个native方法的Java对象实例。如果是静态方法,第二个参数是 jclass,代表Java类本身。
// 实例方法:thiz 指向调用该方法的Java对象
extern "C" JNIEXPORT void JNICALL
Java_com_example_NativeBridge_doSomething(
JNIEnv* env,
jobject thiz) {
// 通过thiz可以访问Java对象的字段和方法
jclass clazz = env->GetObjectClass(thiz);
// ...
}
// 静态方法:clazz 指向Java类
extern "C" JNIEXPORT void JNICALL
Java_com_example_NativeBridge_staticMethod(
JNIEnv* env,
jclass clazz) {
// 通过clazz可以访问静态字段和静态方法
}
我个人习惯在实现JNI函数时,先检查env和thiz/clazz是否为NULL。虽然大多数情况下不会为空,但养成这个习惯能帮你避免一些诡异的崩溃。
1.4 字符串处理
Java的String和C/C++的字符串不是一回事。Java的String是UTF-16编码的Unicode字符串,而C/C++用的是以'\0'结尾的char数组。JNI提供了转换函数:
// Java字符串 -> C字符串
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_NativeBridge_processString(
JNIEnv* env,
jobject thiz,
jstring input) {
// 获取C风格字符串
const char* nativeStr = env->GetStringUTFChars(input, nullptr);
if (nativeStr == nullptr) {
return nullptr; // 内存分配失败
}
// 使用字符串
std::string result = "Processed: ";
result += nativeStr;
// 释放资源
env->ReleaseStringUTFChars(input, nativeStr);
// 返回新的Java字符串
return env->NewStringUTF(result.c_str());
}
这里有个坑:GetStringUTFChars 返回的是UTF-8编码的字符串,但Java内部是UTF-16。转换过程有性能开销。如果你处理大量字符串,可以考虑用 GetStringCritical 和 ReleaseStringCritical 来减少拷贝。
我曾经在项目中忘记调用ReleaseStringUTFChars,导致内存泄漏。JNI的字符串函数分配的内存需要手动释放,不像C++的智能指针那么省心。建议用RAII封装一下:
class JStringGuard {
public:
JStringGuard(JNIEnv* env, jstring str)
: m_env(env), m_str(str) {
m_chars = env->GetStringUTFChars(str, nullptr);
}
~JStringGuard() {
if (m_chars) {
m_env->ReleaseStringUTFChars(m_str, m_chars);
}
}
const char* get() const { return m_chars; }
private:
JNIEnv* m_env;
jstring m_str;
const char* m_chars;
};
1.5 数组处理
数组的处理逻辑和字符串类似。JNI提供了专门的操作函数:
// 处理int数组
extern "C" JNIEXPORT jintArray JNICALL
Java_com_example_NativeBridge_doubleArray(
JNIEnv* env,
jobject thiz,
jintArray input) {
// 获取数组长度
jsize len = env->GetArrayLength(input);
// 获取数组元素指针
jint* elements = env->GetIntArrayElements(input, nullptr);
if (elements == nullptr) {
return nullptr;
}
// 创建输出数组
jintArray result = env->NewIntArray(len);
jint* resultElements = env->GetIntArrayElements(result, nullptr);
// 处理每个元素
for (jsize i = 0; i < len; i++) {
resultElements[i] = elements[i] * 2;
}
// 释放资源
env->ReleaseIntArrayElements(input, elements, JNI_ABORT);
env->ReleaseIntArrayElements(result, resultElements, 0);
return result;
}
注意 ReleaseIntArrayElements 的第三个参数:
0:将修改同步回Java数组并释放缓冲区JNI_ABORT:不同步修改,直接释放缓冲区JNI_COMMIT:同步修改但不释放缓冲区
对于对象数组(比如String数组),需要用 GetObjectArrayElement 和 SetObjectArrayElement 逐个访问:
extern "C" JNIEXPORT void JNICALL
Java_com_example_NativeBridge_printStrings(
JNIEnv* env,
jobject thiz,
jobjectArray stringArray) {
jsize len = env->GetArrayLength(stringArray);
for (jsize i = 0; i < len; i++) {
jstring str = (jstring)env->GetObjectArrayElement(stringArray, i);
const char* nativeStr = env->GetStringUTFChars(str, nullptr);
// 处理字符串...
env->ReleaseStringUTFChars(str, nativeStr);
env->DeleteLocalRef(str); // 释放局部引用
}
}
我建议在处理完每个对象数组元素后,及时调用DeleteLocalRef释放局部引用。JNI的局部引用表默认容量有限(通常是512个),处理大数组时很容易溢出。
知识体系总览
下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:
这张图把整个流程串起来了:Java层用native声明,JNI桥接层负责匹配,C/C++层实现具体逻辑,同时通过JNIEnv操作字符串和数组。你写代码时,脑子里要有这张图,就不会迷失方向。