第一个JNI程序:Java与C++互相调用
好,咱们直接动手。第一个JNI程序,说白了就是让Java和C++能互相喊话。我刚开始学JNI时,觉得这玩意儿挺玄乎——两个完全不同的语言,怎么就能在Android上无缝配合呢?其实底层原理没那么复杂,今天咱们一步步把它拆开。
JNI函数命名规则
先说说命名。JNI函数的命名,有一套严格的规则。你想想看,Java层的方法到了C++层,总得有个统一的寻址方式吧?
规则是这样的:Java_包名_类名_方法名。包名里的点号要换成下划线。举个例子:
// Java层
package com.example.myapp;
public class NativeLib {
public static native String helloFromCpp();
}
// C++层对应的函数名
JNIEXPORT jstring JNICALL
Java_com_example_myapp_NativeLib_helloFromCpp(JNIEnv *env, jclass clazz) {
// ...
}
嗯,这里要注意一点。如果包名或类名里本身就有下划线,那JNI会用_1来转义。我在项目中遇到过有人因为包名带下划线,结果JNI死活找不到函数,排查了半天才发现是命名问题。所以,我个人建议包名和类名里尽量别用下划线,省得给自己挖坑。
javah与javac的使用
接下来是工具链。javac和javah,这两个命令是JNI开发的入门工具。
javac 负责把Java源码编译成.class文件。这个大家应该都熟,不多说。
javah 的作用是自动生成JNI头文件。它会根据Java代码中的native方法声明,生成对应的C/C++函数声明。说白了,就是帮你把命名规则那套繁琐的东西自动化了。
具体用法:
// 1. 编译Java文件
javac com/example/myapp/NativeLib.java
// 2. 生成JNI头文件
javah -jni com.example.myapp.NativeLib
执行完第二步,你会得到一个com_example_myapp_NativeLib.h文件。打开看看,里面就是JNI要求的函数声明。你只需要在C++文件里实现这些函数就行。
生成so库
最后一步,把C++代码编译成so库。在Android NDK环境下,我们通常用CMake或ndk-build来管理编译。
这里我以CMake为例,展示一个最简单的CMakeLists.txt:
cmake_minimum_required(VERSION 3.4.1)
project("mynativelib")
add_library(
mynativelib
SHARED
native-lib.cpp
)
find_library(
log-lib
log
)
target_link_libraries(
mynativelib
${log-lib}
)
然后在build.gradle里配置一下:
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-std=c++11"
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
编译完成后,so文件会出现在app/build/intermediates/cmake目录下。不同CPU架构(armeabi-v7a、arm64-v8a、x86等)会生成对应的so文件。
完整示例:Java与C++互相调用
光说不练假把式。咱们写一个完整的例子,Java调用C++,C++再回调Java。
Java层:
package com.example.myapp;
public class NativeLib {
static {
System.loadLibrary("mynativelib");
}
// C++实现的方法
public static native String helloFromCpp();
// C++会回调这个方法
public static void callbackFromCpp(String message) {
Log.d("NativeLib", "C++回调: " + message);
}
}
C++层(native-lib.cpp):
#include <jni.h>
#include <string>
#include <android/log.h>
#define LOG_TAG "NativeLib"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_NativeLib_helloFromCpp(
JNIEnv *env,
jclass clazz) {
// 获取Java类的jclass
jclass nativeLibClass = env->FindClass("com/example/myapp/NativeLib");
if (nativeLibClass == nullptr) {
LOGD("找不到NativeLib类");
return env->NewStringUTF("error");
}
// 获取callbackFromCpp方法的jmethodID
jmethodID callbackMethod = env->GetStaticMethodID(
nativeLibClass,
"callbackFromCpp",
"(Ljava/lang/String;)V"
);
if (callbackMethod == nullptr) {
LOGD("找不到callbackFromCpp方法");
return env->NewStringUTF("error");
}
// 调用Java方法
jstring message = env->NewStringUTF("Hello from C++!");
env->CallStaticVoidMethod(nativeLibClass, callbackMethod, message);
// 返回结果给Java
return env->NewStringUTF("C++处理完成,已回调Java");
}
这个例子展示了完整的双向调用流程。Java调用C++的helloFromCpp,C++内部又通过JNI回调了Java的callbackFromCpp方法。
核心要点:
- JNI函数命名必须严格遵循规则,否则找不到
- javah可以自动生成头文件,减少手动编写的工作量
- CMake是Android NDK推荐使用的构建工具
- JNIEnv指针是C++与Java交互的桥梁,所有JNI函数都通过它调用
- 记得处理异常和空指针,JNI代码一旦崩溃,整个应用都会挂掉
好了,第一个JNI程序就到这里。说白了,JNI就是一层协议,让Java和C++能互相理解对方在说什么。你只要掌握了命名规则、工具链和基本的调用流程,后面再复杂的场景都是这些基础知识的组合。
下一章,咱们会深入JNI的数据类型转换——Java的String怎么变成C++的std::string?Java的数组怎么传给C++?这些坑,我当年都踩过,到时候一一告诉你。