JNI 即 Java Native Interface,是 Java 编程语言的一部分,它提供了一种机制,使得 Java 代码能够调用其他语言(如 C、C++)编写的函数,也允许 C、C++ 代码调用 Java 方法。这篇文章将介绍 JNI 的使用。
日志
在 NDK 中,如果想要打印日志信息,需要包含 Android 的日志头文件 android/log.h,该头文件定义了日志相关的函数和宏。android/log.h 中定义了多个日志级别对应的函数,常用的有 __android_log_print。为了方便,一般会定义宏函数。代码示例如下:
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "NativeLog"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
LOGV("This is a verbose log.");
LOGD("This is a debug log.");
LOGI("This is an info log.");
LOGW("This is a warning log.");
LOGE("This is an error log.");
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
数据类型转换
由于 Java 和 C++ 的数据类型是不同的,因此Java 和 C++ 之间的相互调用,需要先对数据类型进行转换。
基本数据类型转换表
| Java | Native类型 | 符号属性 | 字长 |
|---|---|---|---|
| boolean | jboolean | 无符号 | 8位 |
| byte | jbyte | 无符号 | 8位 |
| char | jchar | 无符号 | 16位 |
| short | jshort | 有符号 | 16位 |
| int | jint | 有符号 | 32位 |
| long | jlong | 有符号 | 64位 |
| float | jfloat | 有符号 | 32位 |
| double | jdouble | 有符号 | 64位 |
引用数据类型转换表
| Java引用类型 | Native类型 |
|---|---|
| all objects | jobject |
| java.lang.Class | jclass |
| java.lang.String | jstring |
| Object[] | jobjectArray |
| boolean[] | jbooleanArray |
| byte[] | jbyteArray |
| char[] | jcharArray |
| short[] | jshortArray |
| int[] | jintArray |
| long[] | jlongArray |
| float[] | jfloatArray |
| double[] | jdoubleArray |
| java.lang.Throwable | jthrowable |
数据类型转换示例
- 基本数据类型的转换
jni 类型转化为 C++ 类型的示例如下:
// int 转换
jint jniInt = 20;
int cppInt = static_cast<int>(jniInt);
LOGD("cppInt = %d", cppInt);
// float 转换
jfloat jniFloat = 2.718f;
float cppFloat = static_cast<float>(jniFloat);
LOGD("cppFloat = %f", cppFloat);
// boolean 转换
jboolean jniBool = JNI_TRUE;
bool cppBool = jniBool == JNI_TRUE;
LOGD("cppBool = %d", cppBool);
// char 转换
jchar jniChar = 'B';
char cppChar = static_cast<char>(jniChar);
LOGD("cppChar = %c", cppChar);
C++ 类型转化为 jni 类型的示例如下:
// jint 转换
int cppInt = 10;
jint jniInt = static_cast<jint>(cppInt);
LOGD("jniInt = %d", jniInt);
// jfloat 转换
float cppFloat = 3.14f;
jfloat jniFloat = static_cast<jfloat>(cppFloat);
LOGD("jniFloat = %f", jniFloat);
// jboolean 转换
bool cppBool = true;
jboolean jniBool = cppBool? JNI_TRUE : JNI_FALSE;
LOGD("jniBool = %d", jniBool);
// jchar 转换
char cppChar = 'A';
jchar jniChar = static_cast<jchar>(cppChar);
LOGD("jniChar = %c", jniChar);
- 数组类型转换
extern "C" JNIEXPORT jintArray JNICALL Java_com_example_nativedemo_MainActivity_testNativeArray
(JNIEnv* env, jobject, jintArray array) {
// jni 数组类型转化为 C++ 类型
int length = env->GetArrayLength(array);
int cppArray[length];
env->GetIntArrayRegion(array, 0, length, cppArray);
for(int i = 0; i < length; i++) {
LOGD("cppArray index = %d value = %d", i, cppArray[i]);
cppArray[i] += 1;
}
// C++ 数组类型转化为 jni 类型
jintArray jniArray = env->NewIntArray(length);
env->SetIntArrayRegion(jniArray, 0, length, cppArray);
return jniArray;
}
- string 类型转换
在 Java 中 String 是 UTF-16 编码的,因此需要特别处理。代码示例如下:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_nativedemo_MainActivity_testNativeString
(JNIEnv* env, jobject, jstring jniStr) {
// jni -> C++
const char* utfChars = env->GetStringUTFChars(jniStr, nullptr);
std::string cppStr(utfChars);
cppStr.insert(cppStr.length(), "-jni");
LOGD("cppStr = %s", cppStr.c_str());
env->ReleaseStringUTFChars(jniStr, utfChars);
// C++ -> JNI
return env->NewStringUTF(cppStr.c_str());
}
jstring 相关的方法如下所示:
NewString()//从Native字符串得到一个jstring对象
NewStringUTF()//根据Native的一个UTF-8字符串得到一个jstring对象
GetStringChars()//将java string 转换成Unicode字符串
GetStringUTFChars()//将java string转换成UTF-8字符串
ReleaseStringChars()//释放资源,否则会导致JVM内存泄露
ReleaseStringUTFChars()//释放资源,否则会导致JVM内存泄露
方法调用
要在 Java/Kotlin中使用 C++ 的代码,需要声明 native 方法,再加载native库。示例如下:
// java
public class NativeExample {
static {
System.loadLibrary("native-lib");
}
public native int nativeAdd(int a, int b);
}
// kotlin
class NativeExample {
// 声明本地方法
external fun nativeAdd(a: Int, b: Int): Int
companion object {
// 加载本地库
init {
System.loadLibrary("native-lib")
}
}
}
java 调用 C++
Java 调用 C++ 代码需要注册,有两种注册方式,分别是静态注册、动态注册。
- 静态注册(不推荐)
- 编写java文件,编译成 .class
- 使用
javac -h 生成h文件路径 源文件路径命令生成 .h 文件
实现原理:当Java层调用函数(如 native_init())时,它会从对应的JNI库寻找对应的函数(如 Java_android_media_Scanner_native_init()),如果没有就会报错。如果找到则会为这两个函数建立一个关联关系,其实就是保存JNI层函数的函数指针。以后调用这个函数时,直接使用就可以了。
静态注册的不足:
- 需要编译所有声明了
native的函数的java类,并且需要每个为它们生成一个 .h 文件 - javah 生成的JNI函数名特别长
- 初次调用会建立关系,影响运行效率
- 动态注册(推荐)
动态注册的结构
typedef struct {
const char* name;//java的native方法的函数名
const char* signature;//java的native方法的签名信息
void* fnPtr;//JNI层对应函数指针
} JNINativeMethod;
示例代码如下
static const JNINativeMethod gMethods[] = {
...
{
"native_init",
"()V",
(void *)android_media_MediaScanner_native_init
},
{
"native_setup",
"()V",
(void *)android_media_MediaScanner_native_setup
},
{
"native_finalize",
"()V",
(void *)android_media_MediaScanner_native_finalize
},
};
将结构注册的方法是
//这里的className是java类的全路径名
jclass clazz = (*env) ->FindClass(env, className);
//注册关联关系,Android中提供了JNIHelp,其内部有jniRegisterNativeMethods方法封装了这些步骤
(*env)->RegisterNatives(env, clazz, gMethods, numMethods);
当Java层通过System.loadLibrary加载完动态库后,会查找该库的JNI_OnLoad函数。如果有的话,就会调用它。因此我们需要在代码中实现这个函数,并在函数中调用注册结构的方法。
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
...
//注册关联关系
(*env)->RegisterNatives(env, clazz, gMethods, numMethods);
...
return JNI_VERSION_1_4;//必须返回这个值,否则报错
}
JNI层代码中一般要包含
jni.h的头文件。Android源码中提供了JNIHelp.h的帮助头文件,它内部包含了jni.h。所以代码中直接包含JNIHelp.h即可
C++ 调用 java
JavaVM 和 JNIEnv
JavaVM 是 Java 虚拟机(JVM)在本地代码中的抽象表示,是一个指向虚拟机实例的指针。每个进程中只能有一个 JavaVM 实例,它代表了整个 Java 虚拟机环境,提供了与 JVM 本身进行交互的接口。它的作用有:
- 虚拟机生命周期管理:可以用来创建、销毁 Java 虚拟机实例。例如,在本地代码中启动一个新的 Java 虚拟机,或者在程序结束时正确地关闭 Java 虚拟机。
- 线程管理:允许本地线程附加到 Java 虚拟机或者从 Java 虚拟机分离,这样本地线程就可以参与到 Java 环境的操作中。
JNIEnv 则是一个指向本地方法调用接口环境的指针,它代表了 Java 虚拟机在当前线程中的执行环境。每个线程都有自己独立的 JNIEnv 实例,这意味着 JNIEnv 是线程局部的,不能在线程之间共享。它的作用有:
- 访问 Java 类和对象:通过 JNIEnv 可以查找 Java 类、创建 Java 对象、访问 Java 对象的字段和方法。例如,使用 FindClass 方法查找 Java 类,使用 NewObject 方法创建 Java 对象。
- 数据类型转换:负责在 Java 数据类型和本地数据类型之间进行转换,如将 Java 字符串转换为 C 字符串,将 C 整数转换为 Java 整数等。
- 异常处理:提供了检查、抛出和清除 Java 异常的方法,确保在本地代码中正确处理 Java 异常。
通过 JNIEnv 调用 java 代码
C++ 调用 java 代码是通过 JNIEnv 来实现。它的作用就是:调用Java的函数、操作jobject对象等。下图是 JNIEnv 的内部结构。从图中可知,JNIEnv 实际上就是提供了一些JNI系统函数。

代码示例如下:
extern "C" JNIEXPORT void JNICALL Java_com_example_nativedemo_MainActivity_testInvokeJavaMethod
(JNIEnv* env, jobject obj) {
// 获取 MainActivity 的类对象
jclass clazz = env->GetObjectClass(obj);
if (clazz == nullptr) {
LOGD("Failed to get class");
return;
}
// 调用非静态方法 method
jmethodID methodId = env->GetMethodID(clazz, "method", "()V");
if (methodId != nullptr) {
env->CallVoidMethod(obj, methodId);
} else {
LOGD("Failed to get method ID");
}
// 调用静态方法 staticMethod
jmethodID staticMethodId = env->GetStaticMethodID(clazz, "staticMethod", "()V");
if (staticMethodId != nullptr) {
env->CallStaticVoidMethod(clazz, staticMethodId);
} else {
LOGD("Failed to get static method ID");
}
// 释放类对象引用
env->DeleteLocalRef(clazz);
}
内存回收
JNI的引用
在jni规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。
- 局部引用
通过 NewLocalRef 和各种 JNI 接口(如 FindClass、NewObject、GetObjectClass和NewCharArray等)可以创建或者获取局部引用的对象。局部引用会阻止 GC 回收所引用的对象,不能在本地函数中跨函数使用,不能跨线程使用。当函数返回后局部引用所引用的对象会被JVM 自动释放,或调用 DeleteLocalRef 方法主动释放。示例如下:
extern "C" JNIEXPORT void JNICALL Java_com_example_nativedemo_MainActivity_localRefExample(JNIEnv *env, jobject obj) {
// 获取 Java 类的 Class 对象
jclass cls = env->GetObjectClass(obj);
...
// 局部引用在方法返回时自动释放
}
- 全局引用
调用 NewGlobalRef 可以创建全局引用的对象,它会阻 GC 回收所引用的对象。全局引用可以跨方法、跨线程使用。对于全局引用,JVM 不会自动释放,必须调用 DeleteGlobalRef 手动释放。代码示例如下:
// 全局引用变量
jobject globalObj;
extern "C" JNIEXPORT void JNICALL Java_com_example_nativedemo_MainActivity_createGlobalRef(JNIEnv *env, jobject obj) {
// 创建全局引用
globalObj = env->NewGlobalRef(obj);
LOGD("Global reference created");
// 可以正常使用 globalObj
jclass clazz = env->GetObjectClass(globalObj);
// 调用非静态方法 method
jmethodID methodId = env->GetMethodID(clazz, "method", "()V");
if (methodId != nullptr) {
env->CallVoidMethod(globalObj, methodId);
} else {
LOGD("Failed to get method ID");
}
}
extern "C" JNIEXPORT void JNICALL Java_com_example_nativedemo_MainActivity_deleteGlobalRef(JNIEnv *env, jobject obj) {
if (globalObj != NULL) {
// 释放全局引用
env->DeleteGlobalRef(globalObj);
globalObj = NULL;
LOGD("Global reference deleted.");
}
}
- 弱全局引用
调用 NewWeakGlobalRef 可以创建弱全局引用的对象,它不会阻止 GC 回收所引用的对象,可以跨方法、跨线程使用。弱全局引用不会自动释放,在 JVM 认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放;或调用DeleteWeakGlobalRef 手动释放。代码示例如下:
// 弱全局引用变量
jweak weakObj;
extern "C" JNIEXPORT void JNICALL Java_com_example_nativedemo_MainActivity_createWeakGlobalRef(JNIEnv *env, jobject obj) {
// 创建弱全局引用
weakObj = env->NewWeakGlobalRef(obj);
LOGD("Weak global reference created");
// 可以正常使用 weakObj
jclass clazz = env->GetObjectClass(weakObj);
// 调用非静态方法 method
jmethodID methodId = env->GetMethodID(clazz, "method", "()V");
if (methodId != nullptr) {
env->CallVoidMethod(weakObj, methodId);
} else {
LOGD("Failed to get method ID");
}
}
extern "C" JNIEXPORT void JNICALL Java_com_example_nativedemo_MainActivity_deleteWeakGlobalRef(JNIEnv *env, jobject obj) {
if (weakObj != NULL) {
// 释放弱全局引用
env->DeleteWeakGlobalRef(weakObj);
weakObj = NULL;
LOGD("Weak global reference deleted");
}
}
数组对象回收
对于基本类型数组,在获取数组元素指针并使用完后,需要调用 Release<Type>ArrayElements 函数来释放对数组元素的访问权限。代码示例如下:
auto int_arr = env->GetIntArrayElements(jint_arr, nullptr);
// TODO use int_arr
env->ReleaseIntArrayElements(jint_arr, int_arr, 0);
string 对象回收
extern "C" JNIEXPORT jstring JNICALL Java_com_example_nativedemo_MainActivity_testNativeString
(JNIEnv* env, jobject, jstring jniStr) {
const char* utfChars = env->GetStringUTFChars(jniStr, nullptr);
std::string cppStr(utfChars);
cppStr.insert(cppStr.length(), "-jni");
LOGD("cppStr = %s", cppStr.c_str());
// string 对象回收
env->ReleaseStringUTFChars(jniStr, utfChars);
...
}
多线程
JNI/NDK入门指南之JNI多线程回调Java方法_jni层 多任务回调-CSDN博客
异常处理
如果调用JNI的函数出错了,则会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作了。
JNI层函数可以在代码中捕获和修改这些异常
ExceptionOccured:用来判断是否发生异常 ExceptionClear:用来清理当前JNI层发生的异常 ThrowNew:用来向Java层抛出异常
extern "C" JNIEXPORT void JNICALL
Java_com_example_nativedemo_MainActivity_testThrowable(JNIEnv *env, jobject /* this */, jint a, jint b) {
try {
if (b == 0) {
LOGD("Division by zero");
throw std::runtime_error("Division by zero!");
}
int result = a / b;
// 处理结果
} catch (const std::exception& e) {
// 有异常发生,打印异常信息
env->ExceptionDescribe();
// 获取 Java 异常类
jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
if (exceptionClass != nullptr) {
// 抛出 Java 异常
env->ThrowNew(exceptionClass, e.what());
env->DeleteLocalRef(exceptionClass);
}
}
// 检查是否有异常发生
jthrowable exception = env->ExceptionOccurred();
if (exception != nullptr) {
LOGD("Exception Occurred");
// 清除异常,如果不注释掉,则异常不会被抛出,应用不会崩溃
//env->ExceptionClear();
}
}