[NDK/JNI系列05] JNI引用API

简介: [NDK/JNI系列05] JNI引用API
  • 学习了JNI中涉及到引用的API主要分为局部引用和全局引用两类,以及弱全局引用的特殊情况。
  • 学习了如何使用JNI函数来创建、删除、检查和比较不同类型的引用。
  • 学习了如何使用PushLocalFrame()PopLocalFrame()来管理局部引用的生命周期,并可选地返回一个全局引用。

JNI引用API的使用

学习了JNI中涉及到引用的API主要分为局部引用和全局引用两类,以及弱全局引用的特殊情况。局部引用是在函数内部创建和使用的,它们会随着函数返回而自动销毁。全局引用是在函数外部创建和使用的,它们需要手动删除才能释放内存。弱全局引用是一种特殊的全局引用,它不会阻止垃圾回收器回收对象,因此需要在使用前检查其有效性。

JNI中的全局引用、局部引用以及弱全局引用的主要作用是允许native代码访问和操作Java对象,同时管理它们的生命周期。在实际应用中,这些引用类型的使用取决于具体的需求和场景。

全局引用(Global References):

全局引用在native代码中创建,可以在任何线程中使用,不受局部引用帧的限制,只有在显式调用DeleteGlobalRef时才会被释放。这使得可以长时间或跨线程持有Java对象的引用。

举例来说,假设的应用在native代码中使用了一个长时间运行的线程,这个线程需要定期回调Java方法进行状态更新。在这种情况下,可能需要创建一个全局引用来持有Java回调对象,以确保它在整个线程的生命周期内都不会被垃圾收集器回收。

局部引用(Local References):

局部引用是在native方法调用期间创建的引用,它们只在创建它们的线程中有效,并且在native方法返回时会自动被删除。这可以让在临时操作Java对象时不需要手动管理内存。

例如,假设在一个native方法中访问了Java对象的一个字段。可以创建一个局部引用来持有这个字段的值,在方法返回时,局部引用会自动被删除,不需要手动删除它。

弱全局引用(Weak Global References):

弱全局引用是一种特殊的引用,它不会阻止其引用的对象被垃圾回收。它们在native代码中创建,可以在任何线程中使用,但是可能会被垃圾回收。

比如说,的应用可能会缓存一些Java对象以便快速访问。为了防止这些对象占用过多内存,可以使用弱全局引用来引用它们。这样,当内存不足时,垃圾收集器可以回收这些对象,从而避免内存泄漏。

引用类型

生命周期

作用范围

是否需要显式删除

特殊属性

全局引用

长(需要显式删除)

在任何线程中使用

N/A

局部引用

短(native方法返回时自动删除)

只能在创建它的线程中使用

N/A

弱全局引用

长(需要显式删除)

在任何线程中使用

不会阻止其引用的对象被垃圾回收

函数

作用

PushLocalFrame

创建局部引用帧,为即将创建的局部引用预留空间

PopLocalFrame

删除局部引用帧,一次性删除帧中的所有局部引用,不需要逐个删除

以下是一些重要的引用API:

  • NewGlobalRef(JNIEnv *env, jobject obj): 创建一个全局引用。
  • DeleteGlobalRef(JNIEnv *env, jobject globalRef): 删除一个全局引用。
  • NewLocalRef(JNIEnv *env, jobject ref): 创建一个局部引用。
  • DeleteLocalRef(JNIEnv *env, jobject localRef): 删除一个局部引用。
  • PushLocalFrame(JNIEnv *env, jint capacity): 为即将创建的局部引用预留空间。
  • PopLocalFrame(JNIEnv *env, jobject result): 销毁创建的局部引用,并可选地返回一个全局引用。
  • NewWeakGlobalRef(JNIEnv *env, jobject obj): 创建一个弱全局引用。
  • DeleteWeakGlobalRef(JNIEnv *env, jweak ref): 删除一个弱全局引用。
  • IsSameObject(JNIEnv *env, jobject ref1, jobject ref2): 检查两个引用是否指向相同的对象。

完整实例

下面是C++中实现的一个测试函数,用于演示JNI引用API的使用。首先创建了一个全局引用和一个弱全局引用,并在每次调用时检查它们是否有效。然后创建了一些局部引用,并使用PushLocalFrame()PopLocalFrame()来管理它们。最后使用IsSameObject()来比较不同的引用是否指向同一个对象,并返回一个全局引用作为结果。

#include <jni.h>
#include <string>
#include <android/log.h>
 
#define  LOG_TAG    "Chapter05-JNI"
#define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
 
using namespace chapter05; // 使用chapter05命名空间
 
// 定义一些常量,表示类名和方法签名
const char* STRING_CLASS = "java/lang/String";
const char* OBJECT_CLASS = "java/lang/Object";
 
// 定义一些全局变量,表示全局引用和弱全局引用
jobject globalRef = NULL; // 全局引用
jweak weakGlobalRef = NULL; // 弱全局引用
 
// 定义一个辅助函数,用于检查并打印JNI异常
void checkAndPrintException(JNIEnv *env) {
    jthrowable exception = env->ExceptionOccurred(); // 检查是否发生了Java异常
    if (exception != NULL) {
        env->ExceptionDescribe(); // 打印Java异常的堆栈跟踪信息
    }
}
 
// 定义一个测试函数,用于演示JNI引用API的使用
jobject testReferenceAPI(JNIEnv *env) {
    // 创建一个字符串对象
    jstring str = env->NewStringUTF("测试对象引用-obj");
    checkAndPrintException(env);
 
    // 如果全局引用为空,就创建一个全局引用
    if (globalRef == NULL) {
        globalRef = env->NewGlobalRef(str);
        checkAndPrintException(env);
        LOGD("创建了一个全局引用");
    } else {
        LOGD("全局引用已存在");
    }
 
    // 如果弱全局引用为空,就创建一个弱全局引用
    if (weakGlobalRef == NULL) {
        weakGlobalRef = env->NewWeakGlobalRef(str);
        checkAndPrintException(env);
        LOGD("创建了一个弱全局引用");
    } else {
        LOGD("弱全局引用已存在");
    }
 
    // 检查全局引用是否有效
    jboolean isGlobalRefValid = env->IsSameObject(globalRef, NULL);
    if (isGlobalRefValid == JNI_FALSE) {
        LOGD("全局引用有效");
    } else {
        LOGD("全局引用无效");
    }
 
    // 检查弱全局引用是否有效
    jboolean isWeakGlobalRefValid = env->IsSameObject(weakGlobalRef, NULL);
    if (isWeakGlobalRefValid == JNI_FALSE) {
        LOGD("弱全局引用有效");
    } else {
        LOGD("弱全局引用无效");
    }
 
    // 为即将创建的局部引用预留空间
    env->PushLocalFrame(16);
    checkAndPrintException(env);
 
    // 创建一个对象数组,用于存放局部引用
    jobjectArray localRefs = env->NewObjectArray(10, env->FindClass(OBJECT_CLASS), NULL);
    checkAndPrintException(env);
 
    // 创建一些对象,并将它们作为局部引用添加到数组中
    for (int i = 0; i < 10; i++) {
        jobject obj = env->NewStringUTF("Object");
        checkAndPrintException(env);
        env->SetObjectArrayElement(localRefs, i, obj);
        checkAndPrintException(env);
        env->DeleteLocalRef(obj); // 删除不再使用的局部引用
        checkAndPrintException(env);
    }
 
    // 获取数组中的第一个元素,并创建一个新的局部引用
    jobject firstElement = env->GetObjectArrayElement(localRefs, 0);
    checkAndPrintException(env);
    jobject newLocalRef = env->NewLocalRef(firstElement);
    checkAndPrintException(env);
 
    // 比较两个局部引用是否指向同一个对象
    jboolean isSameObject = env->IsSameObject(firstElement, newLocalRef);
    if (isSameObject == JNI_TRUE) {
        LOGD("两个局部引用指向同一个对象");
    } else {
        LOGD("两个局部引用指向不同对象");
    }
 
    // 销毁创建的局部引用,并返回一个全局引用作为结果
    return env->PopLocalFrame(globalRef);
}
 
extern "C" JNIEXPORT jstring JNICALL
Java_com_ln28_jnidemo_Chapter05_stringFromJNI( // 修改函数名前缀,与Chapter05.kt中对应
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
 
extern "C" JNIEXPORT jobject JNICALL
Java_com_ln28_jnidemo_Chapter05_testReferenceAPI( // 修改函数名前缀,与Chapter05.kt中对应
        JNIEnv* env,
        jobject /* this */) {
    return testReferenceAPI(env);
}

在Kotlin中,需要定义和调用原生方法,并使用System.loadLibrary()来加载本地库。下面是Kotlin中实现的一些原生方法和测试代码:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import com.ln28.jnidemo.databinding.ActivityMainBinding
import com.ln28.jnidemo.databinding.Chapter05Binding
class Chapter05 : AppCompatActivity() {
    // 定义一个原生方法,用于调用测试函数
    external fun testReferenceAPI(): Any // 原生方法:测试引用类型的API调用
 
    var TAG = "Chapter05"; // 定义TAG变量用于日志输出
 
    private lateinit var binding: Chapter05Binding
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        this.setTitle("Chapter05")
        binding = Chapter05Binding.inflate(layoutInflater)
        setContentView(binding.root)
 
        // Example of a call to a native method
        binding.sampleText.text = stringFromJNI() // 调用原生方法,返回字符串,并显示在UI中
 
        // 调用测试函数并打印结果
        val result = testReferenceAPI() // 调用原生方法获取引用类型的API调用结果
        printObject(result) // 调用普通方法打印对象信息
    }
 
    // 定义一个普通方法,用于打印对象信息
    fun printObject(obj: Any) {
        Log.d(TAG, "对象为:$obj")
        Log.d(TAG, "对象的类为:${obj.javaClass}")
        Log.d(TAG, "对象的哈希码为:${obj.hashCode()}")
    }
 
    /**
     * 一个由本应用程序打包的 'jnidemo' native库实现的原生方法。
     */
    external fun stringFromJNI(): String // 原生方法:获取一个字符串
 
    companion object {
        // 用于在应用程序启动时加载 'jnidemo' 库。
        init {
            System.loadLibrary("jnidemo")
        }
    }
}

 

疑问和解答

  1. 为什么我需要全局引用,我不能只使用局部引用吗?
    全局引用和局部引用具有不同的生命周期和作用范围。全局引用可以在任何线程中使用,并且需要显式删除以防止内存泄漏,而局部引用只能在创建它的线程中使用,并且在native方法返回时自动删除。因此,如果需要在多个native方法或者线程之间共享一个Java对象,或者需要持有一个Java对象的引用超出一个native方法的生命周期,就需要使用全局引用。
  2. 创建全局引用有什么开销吗?我可以创建多少个全局引用?
    创建全局引用会占用一些内存和CPU时间,但通常这个开销是很小的。然而,如果创建了很多全局引用而没有适时地删除,可能会导致内存泄漏。理论上,可以创建任意数量的全局引用,只要系统的内存允许。
  3. 如果我忘记删除全局引用,会发生什么?这会导致内存泄漏吗?
    是的,如果创建了全局引用但忘记删除,这将导致内存泄漏,因为全局引用会阻止垃圾收集器回收其引用的对象,即使这个对象在Java代码中已经没有其他引用。
  4. 我如何知道何时需要创建全局引用,而何时只需要局部引用?
    需要创建全局引用的情况通常包括:需要在native代码的不同部分共享Java对象,需要在不同的线程中访问Java对象,或者需要持有一个Java对象的引用超过一个native方法的生命周期。如果只是临时访问一个Java对象,并且在方法返回时不再需要这个引用,那么使用局部引用就足够了。
  5. 局部引用的生命周期是怎样的?如果我在一个native方法中创建了一个局部引用,然后在另一个方法中使用它,会发生什么?
    局部引用的生命周期仅限于创建它的native方法。一旦这个native方法返回,所有在这个方法中创建的局部引用都会自动被删除。因此,如果在一个方法中创建了一个局部引用,然后在另一个方法中使用它,可能会得到一个无效的引用,这会导致未定义的行为,如程序崩溃。
  6. 如果我在多线程环境中使用局部引用会有问题吗?
    是的,局部引用只能在创建它的线程中使用。如果尝试在其他线程中使用局部引用,可能会导致未定义的行为。在多线程环境中,应该使用全局引用来共享Java对象。
  7. 弱全局引用和全局引用有什么区别?我何时需要使用弱全局引用?
    弱全局引用是一种特殊的引用,它不会阻止其引用的对象被垃圾回收。也就是说,即使持有一个弱全局引用,垃圾收集器仍然可以在内存不足时回收这个对象。可能需要使用弱全局引用的情况包括:需要缓存一些Java对象,但不希望这些对象占用过多内存。
  8. 如果我持有一个弱全局引用,然后该对象被垃圾回收,我会得到一个无效引用吗?我应该如何处理这种情况?
    是的,如果持有一个弱全局引用,然后该对象被垃圾回收,会得到一个无效的引用。在使用弱全局引用前,应该总是检查这个引用是否仍然有效。可以使用IsSameObject函数来检查一个引用是否有效。
  9. 如何正确地创建和删除全局引用、局部引用和弱全局引用?
    可以使用NewGlobalRefNewLocalRefNewWeakGlobalRef函数来创建全局引用、局部引用和弱全局引用。使用完一个全局引用或弱全局引用后,应该使用DeleteGlobalRefDeleteWeakGlobalRef函数来删除它。局部引用在native方法返回时会自动被删除,不需要手动删除。
  10. PushLocalFramePopLocalFrame函数是做什么的?我何时需要使用它们?

PushLocalFramePopLocalFrame函数用于管理局部引用。PushLocalFrame函数会创建一个局部引用帧,然后可以在这个帧中创建多个局部引用。使用PopLocalFrame函数时,这个局部引用帧中的所有局部引用都会一次性被删除,这样可以避免手动管理每个局部引用的生命周期。如果在一个native方法中创建了很多局部引用,或者在一个循环中反复创建局部引用,使用这两个函数可以帮助更有效地管理内存。

假设有这样一个例子:在一个native方法中访问了一个大型Java数组,可能会这样做:

jobjectArray array = /* a large Java array */;
int len = (*env)->GetArrayLength(env, array);
for (int i = 0; i < len; i++) {
    (*env)->PushLocalFrame(env, 1);
    jobject element = (*env)->GetObjectArrayElement(env, array, i);
    // do something with element
    (*env)->PopLocalFrame(env, NULL);
}

这样,每次循环都会创建一个新的局部引用帧,循环结束时删除该帧及其所有局部引用。这样可以避免在循环期间积累大量的局部引用,防止内存泄漏。

相关文章
|
2月前
|
安全 Java API
【性能与安全的双重飞跃】JDK 22外部函数与内存API:JNI的继任者,引领Java新潮流!
【9月更文挑战第7天】JDK 22外部函数与内存API的发布,标志着Java在性能与安全性方面实现了双重飞跃。作为JNI的继任者,这一新特性不仅简化了Java与本地代码的交互过程,还提升了程序的性能和安全性。我们有理由相信,在外部函数与内存API的引领下,Java将开启一个全新的编程时代,为开发者们带来更加高效、更加安全的编程体验。让我们共同期待Java在未来的辉煌成就!
60 11
|
2月前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
79 11
|
5月前
|
JavaScript API
【Vue3的组合式API】超详细教程,含computed、watch、组件通信、模版引用......
【Vue3的组合式API】超详细教程,含computed、watch、组件通信、模版引用......
|
6月前
|
Java API Android开发
[NDK/JNI系列04] JNI接口方法表、基础API与异常API
[NDK/JNI系列04] JNI接口方法表、基础API与异常API
80 0
|
PHP
PHP实现自制随机图片API- 调用文件夹和引用网络图片
PHP实现随机图片API- 调用文件夹和引用网络图片
194 0
|
6月前
|
JavaScript 前端开发 算法
Vue 3.0 组合式API 模板引用
Vue 3.0 组合式API 模板引用
34 0
|
6月前
|
数据采集 Java API
Java 正则表达式【非贪婪匹配、格式验证、反向引用、API】
Java 正则表达式【非贪婪匹配、格式验证、反向引用、API】
|
JSON 人工智能 机器人
Python|Python引用图灵机器人api
Python|Python引用图灵机器人api
149 0
|
Java API C++
<转>Java调用C/C++编写的第三方dll动态链接库(非native API)--- JNI
http://www.cnblogs.com/AnnieKim/archive/2012/01/01/2309567.html
1598 0