Android NDK ~ Java 和 Native 交互

前言

本文主要内容:

  • 什么是 JNIEnv
  • JNI 实例方法和静态方法
  • Native 操作 Java 对象的属性
  • Native 调用 Java 对象的方法
  • Native 创建 Java 对象并返回
  • Native 返回一个int数组
  • Native 修改 Java 传递进来的数组
  • 判断两个对象的地址是否相同
  • JNI 与 垃圾回收

前面我们已经在《Android NDK ~ 基础入门指南》 介绍了 NDK 开发的基础知识,今天我们开始介绍下 JNI 的常用函数

什么是 JNIEnv

在我们的第一个 NDK 程序中的 stringFromJNI 方法的第一个参数就是 JNIEnv

extern "C" JNIEXPORT jstring JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

那么什么是 JNIEnv 呢?

JNIEnv 是一个与线程相关的,代表 JNI 环境的结构体。JNIEnv 里面封装了一些 JNI 系统函数,用于操作对象和调用函数。

JNIEnv 是线程相关的,所以不能跨线程使用,各自的线程只能使用各自的 JNIEnv。例如我们上面的 stringFromJNI 就可以直接使用它的第一个参数 JNIEnv,但有的时候我们不能直接拿到 JNIEnv,比如在 Native 层的后台线程收到了某个消息,需调用 Java 层的函数时,这个时候没有 JNIEnv,怎么办呢?我们可以使用 JavaVM,它是虚拟机在 JNI 层的代表,不管有多少个线程只有一个 JavaVM,通过 JavaVM 的 AttachCurrentThread 函数来获取当前线程的 JNIEnv 结构体,当后台线程退出时需要通过 JavaVM 的 DetachCurrentThread 函数来释放资源。

下面我们来看下 JNIEnv 常用的函数。

JNI 实例方法

还是以我们的第一个 NDK 程序中的 stringFromJNI 方法为例:

extern "C" JNIEXPORT jstring JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

该方法的第二个参数是 jobject,意思就是说该方法是一个实例方法,Java 层与之对应就是:

public native String stringFromJNI();

JNI 实例静态方法

上面我们提到第二个参数是 jobject ,表明它是实例方法,那么我们将第二个参数改成 jclass 那么该方法就是静态方法了:

// Native 返回一个字符串(静态方法)
extern "C" JNIEXPORT jstring JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_stringFromJNI2(
        JNIEnv *env,
        jclass) {
    std::string hello = "Hello from C++ static method...";
    return env->NewStringUTF(hello.c_str());
}

对应 Java 层方法:

public native static String stringFromJNI2();

Native 操作 Java 对象的属性

主要通过 SetIntFieldGetIntField 方法操作对象的属性

// Native 设置/获取 Java 对象的属性
extern "C" JNIEXPORT jint JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_updateObjProperty(
        JNIEnv *env,
        jobject obj) {
        
    // 获取对象的 class
    jclass jclazz = env->GetObjectClass(obj);
    
    // 获取字段id
    jfieldID jfid = env->GetFieldID(jclazz, "number", "I");

    // 设置属性的值
    env->SetIntField(obj, jfid, COUNT);

    // 获取属性的值
    jint value = env->GetIntField(obj, jfid);

    env->DeleteLocalRef(jclazz);

    return value;
}

对应 Java 层方法:

public native int updateObjProperty();

如果对象的属性为private,Native 层也能操作

Native 调用 Java 对象的方法

主要通过 Call[Type]Method 方法来调用 Java 对象实例方法,如果是静态方法则使用:CallStatic[Type]Method,其中的 type 就是方法的返回值类型:

// Native 调用 Java 对象的方法
extern "C" JNIEXPORT jstring JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_invokeObjMethod(
        JNIEnv *env,
        jobject obj) {
    jclass jclazz = env->GetObjectClass(obj);
    jmethodID jmid = env->GetMethodID(jclazz, "methodForJNI", "(I)Ljava/lang/String;");
    env->DeleteLocalRef(jclazz);
    return (jstring) env->CallObjectMethod(obj, jmid, COUNT);
}

对应的 Java 层代码:

public native String invokeObjMethod();

注意:调用方法的时候需要注意参数是基本数据类型还是引用类型,如果参数为 Integer,不能传递 jint,JNI 是不会自动装箱的

Native 创建 Java 对象并返回

创建对象主要是通过 NewObject 来实现:

// Native 创建 Java 对象并返回
extern "C" JNIEXPORT jobject JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_createObj(
        JNIEnv *env,
        jobject) {
    jclass jclazz = env->FindClass("com/chiclaim/androidnative/jni/User");
    jmethodID jmid = env->GetMethodID(jclazz, "<init>", "(Ljava/lang/String;)V");
    jstring username = env->NewStringUTF("Chiclaim");
    jobject user = env->NewObject(jclazz, jmid, username);
    env->DeleteLocalRef(jclazz);
    return user;
}

对应的 Java 层代码:

public native User createObj();

注意:构造方法的名字为 <init>

Native 返回一个int数组

主要是通过 NewIntArray 函数创建数组,SetIntArrayRegion 函数为数组设置值:

const jint COUNT = 10;
// Native 返回一个int数组
extern "C" JNIEXPORT jintArray JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_getIntArray(
        JNIEnv *env,
        jobject) {
    jintArray _intArray = env->NewIntArray(COUNT);
    jint tmpArray[COUNT];
    for (jint i = 0; i < COUNT; i++) {
        tmpArray[i] = i;
    }
    env->SetIntArrayRegion(_intArray, 0, COUNT, tmpArray);
    return _intArray;
}

对应的 Java 代码:

public native int[] getIntArray();

Native 修改 Java 传递进来的数组

通过 GetIntArrayElements 获取数组的值,然后修改响应的值,最后通过 SetIntArrayRegion 方法将元素重新设置到原来的数组中去:

const int VALUE = 100;
// Native 修改 Java 传递进来的数组
extern "C" JNIEXPORT void JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_updateIntArray(
        JNIEnv *env,
        jobject,
        jintArray intArray) {
    jboolean isCopy = static_cast<jboolean>(false);
    jint *arr = env->GetIntArrayElements(intArray, &isCopy);
    jint length = env->GetArrayLength(intArray);
    arr[0] = VALUE;

    env->SetIntArrayRegion(intArray, 0, length, arr);
    
    env->ReleaseIntArrayElements(intArray, arr, JNI_ABORT);
}

对应的 Java 代码:

public native void updateIntArray(int[] array);

判断两个对象的地址是否相同

判断两个对象的地址是否相同不能使用 ==,要是用 IsSameObject 函数:

extern "C" JNIEXPORT jboolean JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_equals(
        JNIEnv *env, 
        jobject thiz, 
        jobject user1,
        jobject user2) {
    return env->IsSameObject(user1, user2);
}

对应的 Java 代码:

public native boolean equals(User user1,User user2);

垃圾回收

在 Java 中有两种数据类型,一个是基本数据类型(Primitive Type),一种是引用类型。对于基本数据类型,Java 和 native code 之间是通过拷贝的方式进行传递,而引用类型传递的是引用。

Java 对象传递给 native code,虚拟机需要对该对象进行跟踪,以便将来被垃圾回收器回收掉。所以在 native code 中需要有一种方法通知虚拟机我不再需要这个对象了。

JNI 将对象引用分为两大类,一个是局部引用,一个是全局引用。

局部引用会在 Native 方法执行完毕后自动释放。传递给 Native 方法的参数就是局部引用,Native 方法返回的 Java 对象也属于局部引用。

而全局引用会一直保留着这个对象的引用,直到显式的调用 JNI 方法释放该引用。可以通过一个局部引用创建一个全局引用。JNI 通过 NewGlobalRefDeleteGlobalRef 方法来创建全局引用和删除全局引用:

jobject holdObj = NULL;

extern "C" JNIEXPORT  void JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_holdUser(
        JNIEnv *env, jobject thiz, jobject user) {
    // 通过局部引用 user 创建全局引用
    holdObj = env->NewGlobalRef(user);
}

需要注意,不能将局部引用直接赋值给全局引用:

jobject holdObj = NULL;

extern "C" JNIEXPORT  void JNICALL
Java_com_chiclaim_androidnative_jni_JNIHolder_holdUser(
        JNIEnv *env, jobject thiz, jobject user) {
    holdObj = user; 
}

否则虚拟机会报错:

JNI DETECTED ERROR IN APPLICATION: use of invalid jobject 0x7fefe45918

不用了这个全局引用需要删除它:

env->DeleteGlobalRef(holdObj);

既然局部引用 native 方法执行完毕后会自动释放,是不是就代码不用处理局部引用呢?大部分情况是这样的,但是如果遇到类似如下情况还是需要手动释放局部引用:

  • native 方法需要访问一个大的 Java 对象,从而需要创建一个局部引用,在该方法里需要做一些计算任务,那么这个大对象不会被释放,哪怕计算任务不再需要这个对象了,因为 native 方法还没有还行完毕。这个时候在执行计算任务之前,可以通过 DeleteLocalRef 方法将大对象的引用删除。

  • native 方法创建非常多的局部引用,但同一时间又不会使用这么多的引用,虚拟机为了跟踪这些局部引用需要花费很多空间,可能会导致内存溢出。例如,在 native 方法中遍历一个很大的对象数组,遍历的时候需要获取每个元素,每个元素就是一个局部引用,在遍历的时候,用完一个局部引用就应该删除它,因为遍历下一个元素时,上一个元素的引用就不需要了。

为了实现局部引用,虚拟机会创建一个“登记中心”用于控制 Java 对象过渡到 native 方法。这个“登记中心”会映射一个局部引用到Java对象,这样就可以阻止垃圾回收器将该 Java 对象。所有传递给 native 方法的 Java 对象,包括 native 方法返回的 Java 对象都会自动注册到“登记中心”,native 方法 return 后,“登记中心”也会自动移除该对象,以便被垃圾回收器回收它。“登记中心” 可以是 hash table 或 linked list。

小结

本文只介绍了最常用的一些 JNI 函数使用方法和注意事项,方便日后查阅和完善,更多的 JNI 函数可以查阅官方文档:https://docs.oracle.com/javase/9/docs/specs/jni/index.html

Reference


如果你觉得本文帮助到你,给我个关注和赞呗!

与此同时,我编写了一份: 超详细的 Android 程序员所需要的技术栈思维导图

如果有需要可以移步到我的 GitHub -> AndroidAll,里面包含了最全的目录和对应知识点链接,帮你扫除 Android 知识点盲区。 由于篇幅原因只展示了 Android 思维导图:超详细的Android技术栈

发布了161 篇原创文章 · 获赞 1125 · 访问量 125万+

猜你喜欢

转载自blog.csdn.net/johnny901114/article/details/101124117
今日推荐