Android NDK(二)JNI的使用

393 阅读11分钟

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++ 之间的相互调用,需要先对数据类型进行转换。

基本数据类型转换表

JavaNative类型符号属性字长
booleanjboolean无符号8位
bytejbyte无符号8位
charjchar无符号16位
shortjshort有符号16位
intjint有符号32位
longjlong有符号64位
floatjfloat有符号32位
doublejdouble有符号64位

引用数据类型转换表

Java引用类型Native类型
all objectsjobject
java.lang.Classjclass
java.lang.Stringjstring
Object[]jobjectArray
boolean[]jbooleanArray
byte[]jbyteArray
char[]jcharArray
short[]jshortArray
int[]jintArray
long[]jlongArray
float[]jfloatArray
double[]jdoubleArray
java.lang.Throwablejthrowable

数据类型转换示例

  • 基本数据类型的转换

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++ 代码需要注册,有两种注册方式,分别是静态注册、动态注册。

  • 静态注册(不推荐)
  1. 编写java文件,编译成 .class
  2. 使用 javac -h 生成h文件路径 源文件路径 命令生成 .h 文件

实现原理:当Java层调用函数(如 native_init())时,它会从对应的JNI库寻找对应的函数(如 Java_android_media_Scanner_native_init()),如果没有就会报错。如果找到则会为这两个函数建立一个关联关系,其实就是保存JNI层函数的函数指针。以后调用这个函数时,直接使用就可以了。

静态注册的不足:

  1. 需要编译所有声明了 native 的函数的java类,并且需要每个为它们生成一个 .h 文件
  2. javah 生成的JNI函数名特别长
  3. 初次调用会建立关系,影响运行效率
  • 动态注册(推荐)

动态注册的结构


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 本身进行交互的接口。它的作用有:

  1. 虚拟机生命周期管理:可以用来创建、销毁 Java 虚拟机实例。例如,在本地代码中启动一个新的 Java 虚拟机,或者在程序结束时正确地关闭 Java 虚拟机。
  2. 线程管理:允许本地线程附加到 Java 虚拟机或者从 Java 虚拟机分离,这样本地线程就可以参与到 Java 环境的操作中。

JNIEnv 则是一个指向本地方法调用接口环境的指针,它代表了 Java 虚拟机在当前线程中的执行环境。每个线程都有自己独立的 JNIEnv 实例,这意味着 JNIEnv 是线程局部的,不能在线程之间共享。它的作用有:

  1. 访问 Java 类和对象:通过 JNIEnv 可以查找 Java 类、创建 Java 对象、访问 Java 对象的字段和方法。例如,使用 FindClass 方法查找 Java 类,使用 NewObject 方法创建 Java 对象。
  2. 数据类型转换:负责在 Java 数据类型和本地数据类型之间进行转换,如将 Java 字符串转换为 C 字符串,将 C 整数转换为 Java 整数等。
  3. 异常处理:提供了检查、抛出和清除 Java 异常的方法,确保在本地代码中正确处理 Java 异常。

通过 JNIEnv 调用 java 代码

C++ 调用 java 代码是通过 JNIEnv 来实现。它的作用就是:调用Java的函数、操作jobject对象等。下图是 JNIEnv 的内部结构。从图中可知,JNIEnv 实际上就是提供了一些JNI系统函数。

image.png

代码示例如下:

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();
    }
}

参考