在NDK开发中如何正确创建JNI方法
在Android NDK开发中,Java Native Interface (JNI) 允许Java代码与C/C++代码交互。正确创建JNI方法对于确保Java能够正确调用本地代码至关重要。本文将详细介绍如何在C++文件中创建JNI方法。
JNI方法命名规则
JNI方法的命名遵循严格的约定,格式如下:
Java_{包名}_{类名}_{方法名}
其中:
- 包名中的点(.)要替换为下划线(_)
- 类名和方法名保持不变
示例解析
让我们分析你提供的示例代码:
#include <jni.h>
#include <string>
#include <unistd.h>extern "C" {
JNIEXPORT jstring JNICALL
Java_com_example_MainActivity_getString(JNIEnv *env, jobject obj) {std::string cppStr = "Hello JNI";return env->NewStringUTF(cppStr.c_str()); // 必须用 JNIEnv 创建 Java String
}
}
这个例子中:
Java
- 固定前缀,表示这是一个JNI方法com_example
- 包名com_example
的转换形式MainActivity
- 包含native方法的Java类名getString
- Java中定义的native方法名
创建JNI方法的步骤
1. 确定Java端的native方法声明
首先在Java类中声明native方法:
package com_example;public class MainActivity extends AppCompatActivity {// 声明native方法public native String getString();// 加载包含实现的本地库static {System.loadLibrary("native-lib");}
}
2. 在C++中实现对应的方法
根据Java端的声明,在C++文件中实现对应的方法:
#include <jni.h>
#include <string>extern "C" {
JNIEXPORT jstring JNICALL
Java_com_example_MainActivity_getString(JNIEnv *env, jobject obj) {std::string cppStr = "Hello JNI";return env->NewStringUTF(cppStr.c_str()); // 必须用 JNIEnv 创建 Java String
}
}
3. 关键元素说明
extern "C"
:确保C++编译器按C语言方式处理函数名(避免名称修饰)JNIEXPORT
:宏定义,确保方法在动态库中可见JNICALL
:与调用约定相关的宏jstring
:返回类型,对应Java的StringJNIEnv*
:指向JNI环境的指针,提供访问JNI功能的方法jobject
:调用该native方法的Java对象引用(相当于Java中的this)
常见问题解决
- UnsatisfiedLinkError:通常是由于方法名不匹配导致的,检查包名、类名和方法名是否完全一致
- 方法找不到:确保方法签名正确,包括参数和返回类型
- 特殊字符处理:如果类名包含
$
(如内部类),需要替换为_00024
自动化工具
为了避免手动编写容易出错的长方法名,可以使用以下方法:
- 使用
javah
工具自动生成头文件 - 在Android Studio中,定义好Java native方法后,按Alt+Enter可以让IDE帮你生成对应的C++方法声明
** extern "C"
的作用**
(1)防止 C++ 的名称修饰(Name Mangling)
- C++ 支持函数重载,编译器会对函数名进行修饰(Name Mangling),在编译后的二进制文件中,函数名会被修改成包含参数和返回类型的唯一标识符。
- 例如,
int foo(int)
可能被编译成_Z3fooi
,而float foo(float)
变成_Z3foof
。
- 例如,
- 但 JNI 要求函数名必须严格按照
Java_包名_类名_方法名
的格式,不能有任何改动,否则 Java 在加载动态库时会找不到对应的函数,导致UnsatisfiedLinkError
。 extern "C"
告诉 C++ 编译器:“不要对这个函数进行名称修饰,保持原样”,这样 JNI 才能正确链接到它。
(2)确保 C 语言风格的链接方式
- C 语言没有函数重载,所以它的函数名在编译后不会改变。
- JNI 底层是用 C 语言实现的,所以 JNI 调用的函数必须符合 C 语言的命名规则。
extern "C"
强制让 C++ 代码以 C 语言的方式进行编译和链接,从而兼容 JNI 的调用机制。
为什么 JNI 需要 extern "C"
?
- Java 通过动态链接库(
.so
或.dll
)调用 C/C++ 函数时,必须精确匹配函数名。 - 如果没有
extern "C"
,C++ 的函数名会被编译器修改,导致 Java 在运行时找不到对应的函数,报错:java.lang.UnsatisfiedLinkError: No implementation found for ...
- 使用
extern "C"
后,函数名保持不变,例如:
编译后,函数名仍然是extern "C" JNIEXPORT jstring JNICALL Java_com_example_MainActivity_helloFromJNI(JNIEnv* env, jobject thiz) {return env->NewStringUTF("Hello from C++!"); }
Java_com_example_MainActivity_helloFromJNI
,Java 可以正确调用它。
如何使用 extern "C"
?
(1)单个函数
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_MainActivity_getMessage(JNIEnv* env, jobject thiz) {return env->NewStringUTF("Hello from C++!");
}
(2)多个函数(用 {}
包裹)
extern "C" { // 告诉编译器,以下所有函数都按 C 语言方式编译JNIEXPORT jstring JNICALLJava_com_example_MainActivity_getMessage(JNIEnv* env, jobject thiz) {return env->NewStringUTF("Hello from C++!");}JNIEXPORT jint JNICALLJava_com_example_MainActivity_add(JNIEnv* env, jobject thiz, jint a, jint b) {return a + b;}
}
如果不加 extern "C"
会怎样?**
- C++ 编译器会修改函数名,例如:
可能会被编译成类似// C++ 代码 JNIEXPORT jstring JNICALL Java_com_example_MainActivity_getMessage(JNIEnv* env, jobject thiz);
_Z30Java_com_example_MainActivity_getMessageP7JNIEnv_P8_jobject
的形式。 - Java 调用时,仍然会按照
Java_com_example_MainActivity_getMessage
查找,结果找不到,导致UnsatisfiedLinkError
。
为什么#include <jni.h> #include <unistd.h>可以放在extern "C"里面
** #include
和 extern "C"
的关系**
#include
是预处理器处理的,它在编译之前就被展开(即把头文件的内容复制到当前文件)。extern "C"
是编译器处理的,它影响的是编译后的函数名修饰和链接方式。- 因此,
#include
可以放在extern "C"
里面或外面,对最终代码没有影响(只要包含的头文件正确)。
为什么有人会把 #include
放在 extern "C"
里面?
(1)确保头文件里的函数以 C 方式编译
如果某个头文件(例如 jni.h
或第三方 C 库)本身是用 C 语言编写的,但被 C++ 代码包含,那么:
- 如果头文件没有自带
extern "C"
保护,可能会导致 C++ 编译器错误地尝试对 C 函数进行名称修饰(Name Mangling),导致链接错误。 - 因此,手动用
extern "C"
包裹#include
,可以强制让该头文件里的所有声明按 C 方式编译。
示例
extern "C" {#include <jni.h> // 确保 jni.h 里的 JNI 函数按 C 方式编译#include <unistd.h> // 确保 unistd.h 里的 C 函数(如 sleep())按 C 方式编译
}
更规范的做法:让头文件自己处理 extern "C"
大多数标准 C 头文件(如 <jni.h>
和 <unistd.h>
)已经自带了 extern "C"
保护,例如:
-
jni.h 内部通常会有:
#ifdef __cplusplus extern "C" { #endif// JNI 函数声明...#ifdef __cplusplus } #endif
这样,无论你是用 C 还是 C++ 包含它,都能正确编译。
-
unistd.h(POSIX 标准头文件)通常也有类似的保护。
所以,即使你不手动加 extern "C"
,直接 #include <jni.h>
也能正常工作。
推荐写法
(1)如果头文件已经保护,直接 #include
在外面
#include <jni.h> // jni.h 自带 extern "C" 保护
#include <unistd.h> // unistd.h 也自带保护extern "C" {// 只包裹需要导出的 JNI 函数JNIEXPORT void JNICALL Java_com_example_NativeLib_nativeMethod(JNIEnv* env, jobject obj);
}
(2)如果头文件没有 extern "C"
保护(罕见情况),手动包裹
extern "C" {#include "my_legacy_c_lib.h" // 假设这个头文件没有 extern "C" 保护
}// 然后正常写 JNI 函数...