美文网首页
JNI 实现与原生代码通信

JNI 实现与原生代码通信

作者: Yue_Q | 来源:发表于2019-02-28 11:04 被阅读0次

目录

  • 1.JNI 字符串操作
  • 2.JNI 数组
  • 3.NIO 操作
  • 4.访问域,获得成员函数,与调用方法。
  • 5.异常处理
  • 6.局部和全局引用
  • 7.线程

1. JNI 字符串操作

Java 字符串 转化 C 字符串,随后释放C字符串。

#include <jni.h>
#include <string>
// 打印 Log 日志
#include <android/log.h>
#define LOG_TAG   "GOODLUCK"
#define LOGD(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

using namespace std;


extern "C" JNIEXPORT jstring JNICALL
Java_com_example_q9163_myndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject  /* this */) {

    // 创建 Java 字符串
    jstring javaString = nullptr;
    // 初始化 Java 字符串
    javaString = env->NewStringUTF("hello World!");

//    JAVA 字符串转化 C 字符串
     const char* str = env->GetStringUTFChars(javaString, 0);

    if (nullptr != str) { // null

        LOGD("Java String = %s", str);

        if (JNI_TRUE == isCopy) {
            LOGD("C string is a copy of the Java string");
        } else {
            LOGD("C string points to actual string ");
        }

    } 

    env->ReleaseStringUTFChars(javaString, str);// 释放字符串, 否者造成内存泄漏
    return javaString;
}

GetStringChars 和 GetStringUTFChars 函数中的第三个参数需要更进一步的解释:

const jchar * GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);

当从 JNI 函数 GetStringChars 中返回得到字符串B时,如果B是原始字符串java.lang.String 的拷贝,则isCopy被赋值为 JNI_TRUE。
如果B和原始字符串指向的是JVM中的同一份数据,则 isCopy被赋值为 JNI_FALSE。
当 isCopy值为JNI_FALSE时,本地代码决不能修改字符串的内容,否则JVM中的原始字符串也会被修改,这会打破 JAVA语言中字符串不可变的规则。
通常,因为你不必关心 JVM 是否会返回原始字符串的拷贝,你只需要为 isCopy传递NULL作为参数。


2. JNI 数组

    1. 创建 JAVA 数组,与 C 数组转化
    1. 获取 Java 数组的头指针
#include <jni.h>
#include <string>
// 打印 Log 日志
#include <android/log.h>
#define LOG_TAG   "GOODLUCK"
#define LOGD(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

using namespace std;

/**
 * 1. 创建 JAVA 数组,与 C 数组转化
 * 2. 获取 Java 数组的头指针
 */
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_q9163_myndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject  /* this */) {
    jintArray javaArray = env->NewIntArray(10);// 创建 JAVA 数组

    jint nativeArray[10];
    env->GetIntArrayRegion(javaArray, 0, 10, nativeArray);// 从 java 数组复制到 C 数组
    // 注意:此过程会有耗时操作,当数组很大,应该获取区域元素而不是整体
    env->SetIntArrayRegion(javaArray, 0, 10, nativeArray);// 从 C 数组复制到 java 数组

    /**
     *  指针操作
     *  1. 获取数组指针
     */
     jint* nativeDirectArray;
     jboolean isCopy;
//     获得指向 JAVA 数组的指针
     nativeDirectArray = env->GetIntArrayElements(javaArray, &isCopy);
//     释放指向 Java 数组元素的指针
    env->ReleaseIntArrayElements(javaArray, nativeDirectArray, 0);


    jstring  javaString = env->NewStringUTF("C++");
    return javaString;
}


3. NIO 操作

   原生 I/O 在缓冲管理区、大规模网络文件和 I/O 字符集支持方面性能有所改进。适合在原生代码和 Java 应用程序之间传送大量数据。

/**
 * 1. 创建直接字符缓冲区,使用完后注意回收
 */
extern "C" JNIEXPORT void JNICALL
Java_com_example_q9163_myndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject  /* this */) {
    // C 字节数组创建字节缓冲区,
    unsigned char* buffer = (unsigned char *) malloc(1024);
    jobject directBuffer = env->NewDirectByteBuffer(buffer, 1024);

    // 通过 Java 字节缓冲区获取原生数组
    unsigned char* javaBuffer = (unsigned char *)env->GetDirectBufferAddress(directBuffer);

    return ;
}

4. 访问域,获得成员函数,与调用方法。

  Java 两种访问域:实例域和静态域,类的每个实例都有自己实例域的副本,而一个类的所有实例共享一个静态域。

class kotlinClass {
    /**
     * 实例域
     */
    private val instanceField = "Instance Field"

    companion object {// 静态域
        private val staticField = "Static Field"
    }
}

(1) 获取域ID
参考:http://jackzhang.info/2018/07/03/JNI-%E4%B8%AD-CC++-%E5%8F%8D%E5%B0%84%E8%B0%83%E7%94%A8Java%E6%96%B9%E6%B3%95/

获取引用类,并转化为字符串
在内存溢出的情况下,以下函数返回为NULL ,此时原生代码不会执行

  // 对象引用获得类
     jclass clazz = env->FindClass("com/example/q9163/myndk/MainActivity");
     jclass obj2_class = env->GetObjectClass(obj);
// 实例域ID
     jfieldID instanceFieldID = env->GetFieldID(obj2_class, "instanceField", "Ljava/lang/String;");
     auto str = (jstring)env->GetObjectField(obj, instanceFieldID);//转化为 java 字符串
// 静态域ID
     jfieldID staticFieldID = env->GetStaticFieldID(clazz, "staticField", "Ljava/lang/String;");
     auto instanceField = (jstring *)env->GetObjectField(obj2, instanceFieldID);// 静态域转化字符串
     //Ljava/lang/String; 表明域的类型是 String

注意:为了提高程序性能,可以缓存域ID,一般总是使用最频繁的域ID
建议:可以使用参数的方式传递,而不是让原生代码回到Java 中。

(2) 调用方法
使用参数传递对象,获取实例(静态)方法的ID,调用方法。
注意: Java 和原生代码之间转换代价,应根据实际情况考虑,尽可能提高系统性能。

/**
 * 1. 使用参数传递对象
 * 2. 获取实例方法的ID
 * 3. 获取静态方法的ID
 *
 * 4. 调用方法
 */
extern "C" JNIEXPORT void JNICALL
Java_com_example_q9163_myndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject  obj,
        jobject obj2 /* this */) {

    // 对象引用获得类
//     jclass clazz = env->FindClass("com/example/q9163/myndk/MainActivity");
     jclass obj2_class = env->GetObjectClass(obj2);
//    获得实例方法ID,可以缓存ID提高性能
    jmethodID instanceMethodID = env->GetMethodID(obj2_class, "getInstanceField", "()Ljava/lang/String;");// 无参数,返回值String类型
//    调用实例方法
    auto instanceMethod = env->CallObjectMethod(obj2_class, instanceMethodID);
//    获得静态方法ID
    jmethodID staticMethodID = env->GetStaticMethodID(obj2_class, "staticMethod", "()Ljava/lang/String;");
//    调用静态方法
    auto staticMethod = env->CallStaticObjectMethod(obj2_class, staticMethodID);

}

Java签名映射.png

5. 异常处理

  在 Java 中,当抛出一个异常,虚拟机会停止执行代码并进入栈反向检查能处理特定异常类型的代码块(进入 catch 语句),虚拟机清除异常并将控制权交给异常程序处理。

 抛出异常的例子

public class ThrowClass {
    // 抛出异常的方法
    private void throwingMethod() throws NullPointerException {
        throw new NullPointerException("NULL Pointer");
    }
    // 访问原生方法
    private native void accessMethod();
}

5.1 异常捕获
 Native 实现

**
 * 1. 原生代码异常处理,异常程序创建与清除
 */

extern "C"
JNIEXPORT void JNICALL
Java_com_example_q9163_myndk_java_ThrowClass_accessMethod(JNIEnv *env, jobject instance) {

    // 对象引用获得类
    jclass clazz = env->FindClass("com/example/q9163/myndk/java/ThrowClass");
    jmethodID throwingMethodId = env->GetMethodID(clazz, "throwingMethod", "()V;");

    // 原生代码异常处理
    env->CallVoidMethod(instance, throwingMethodId);
    jthrowable ex = env->ExceptionOccurred();// 查询虚拟机中有挂起异常
    if (nullptr != ex) {// 异常处理程序清楚
        env->ExceptionClear();
    }
}

5.1 异常抛出
  JNI 也允许原生代码抛出异常,因为异常是JAVA类,

  1. 先调用 FindClass 函数找到异常类。
  2. 用 ThrowNew 函数初始化并抛出异常。
/**
 * 1. 抛出异常
 */

extern "C"
JNIEXPORT void JNICALL
Java_com_example_q9163_myndk_java_ThrowClass_accessMethod(JNIEnv *env, jobject instance) {
    jclass clazz = env->FindClass("java/lang/NullPointerException");
    if (nullptr != clazz) {
        env->ThrowNew(clazz, "Exception message");
    }
}

注意:原生代码不受虚拟机控制,抛出异常不会停止原生函数的执行并把控制权转交给异常处理程序。抛出异常时,原生代码应该释放自己已分配的资源,例如内存及合适的返回值等,通过 JNIEnv 接口获得的引用是局部引用,一旦返回原生函数他们将被释放。


6. 局部和全局引用

  JNI 提供了一组函数允许原生代码显示的管理对象引用和使用期间原生代码。JNI 支持三种引用:局部引用、全局引用、弱全局引用

6.1 局部引用
  大部分 JNI 函数返回局部引用,局部引用不能再后续调用被缓存和重用,
主要原因:它们的使用期限仅原生方法,一旦原生方法返回、局部引用被释放

删除一个局部引用

/**
 * 1 FindClass 返回一个局部引用
 * 2. 删除一个局部引用
 */
extern "C" JNIEXPORT void JNICALL
Java_com_example_q9163_myndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject  obj,
        jobject obj2 /* this */) {
        jclass clazz = env->FindClass("java/lang/String");// 这里返回一个局部引用
        /***/
        env->DeleteLocalRef(clazz);

}

   虚拟机允许原生代码创建至少 16 个局部引用。使用时应注意删除未用的引用。可以通过 EnsureLocalCapacity 函数申请更多局部引用。

6.2 全局引用
  通过 NewGlobalRef局部引用初始化为全局引用。

/**
 * 1 全局引用
 */
extern "C" JNIEXPORT void JNICALL
Java_com_example_q9163_myndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject  obj,
        jobject obj2 /* this */) {
    // 创建局部引用
    jclass localClass = env->FindClass("java/lang/String");
    // 创建全局引用
    auto GlobalClass = (jclass )env->NewGlobalRef(localClass);
    //***/
    env->DeleteLocalRef(localClass);// 删除局部引用
    env->DeleteGlobalRef(GlobalClass);// 删除全局引用
}

6.3 全局弱引用
  与全局引用不同是,弱全局引用不会阻止潜在的对象被垃圾回收
  通过 NewWeakGlobalRef 函数对弱全局引用初始化。通过 IsSameObject 函数检查是否被回收。

/**
 * 1. 全局弱引用
 * 2. 检验有效
 */
extern "C" JNIEXPORT void JNICALL
Java_com_example_q9163_myndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject  obj,
        jobject obj2 /* this */) {
    // 创建局部引用
    jclass localClass = env->FindClass("java/lang/String");
    // 创建全局引用
    auto weakGlobalClass = (jclass )env->NewWeakGlobalRef(localClass);
    // 检验弱全局引用变量是否有效
    if (JNI_FALSE == env->IsSameObject(weakGlobalClass, NULL)) {
        /* 对象处于活动状态可用 */
    } else {
        /* 对象不可用、被垃圾回收器回收 */
    }
    //***/
    env->DeleteLocalRef(localClass);// 删除局部引用
    env->DeleteWeakGlobalRef(weakGlobalClass);// 删除弱全局引用
}

7. 原生线程

 虚拟机不知道原生线程,因为他们不能跟 Java 构件直接通信。原生线程附着在虚拟机上。

 JNI 通过 JavaVM 接口指针提供了 AttachCurrentThread 函数让原生线程附着在虚拟机
上。

/**
 * 1. 线程的附着与分离
 */
extern "C" JNIEXPORT void JNICALL
Java_com_example_q9163_myndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject  obj,
        jobject obj2 /* this */) {
    JavaVM *cachedJvm ;
    /* 将线程附着在虚拟机上 */
    cachedJvm->AttachCurrentThread(&env, NULL);
    /* 将线程与虚拟机分离 */
    cachedJvm->DetachCurrentThread();
}

AttachCurrentThread: 允许当前线程获得 JNIEnv 有效接口指针,多次调用函数不会有影响。
DetachCurrentThread: 当前线程与虚拟机分离

相关文章

  • JNI 实现与原生代码通信

    目录 1.JNI 字符串操作 2.JNI 数组 3.NIO 操作 4.访问域,获得成员函数,与调用方法。 5.异常...

  • JNI与原生代码之间的通信

    JNI定义 JNI是Java程序设计语言功能最强的特征,它允许Java类的某些方法原生实现,同时让它们能够像普通J...

  • Android 添加JNI 开发能力

    JNI 与 NDK 区别JNI:JNI是一套编程接口,用来实现Java代码与本地的C/C++代码进行交互;NDK:...

  • jni新手笔记二:java调用c++

    jni作为 java代码与c/c++ 代码之间的桥梁,通过jni可以实现java代码和c/c++代码互相调用 在 ...

  • JNI 常见用法

    一、Java 代码 和JNI代码通信 Java代码通过JNI接口 调用 C/C++方法 1、首先我们需要在Java...

  • NDK--JNI开发

    使用JNI与Native code通信 什么事JNI? JNI是Java native interface的缩写,...

  • UVC系列5-编写Android jni代码实现控制PTZ

    在Android kernel层完成定制之后,需要写app实现对摄像头的控制,主要通过jni代码实现。在jni代码...

  • Flutter通过BasicMessageChannel实现Fl

    Flutter 与 Android iOS 原生的通信有以下三种方式 BasicMessageChannel 实现...

  • Flutter_Channel

    Platform Channel 机制 Platform Channel机制用来实现Flutter与原生的通信。P...

  • ★10.异常处理

    简述 写 JNI 代码时,需要时刻考虑每一个 JNI函数 可能抛出的异常。 在原生代码中,一旦发生异常,需要马上处...

网友评论

      本文标题:JNI 实现与原生代码通信

      本文链接:https://www.haomeiwen.com/subject/vyafuqtx.html