《Android的设计与实现:卷I》——第2章 2.5 JNI操作Java对象

简介: 本节书摘来自华章出版社《Android的设计与实现:卷I》——第2章,第2.5节。作者: 杨云君著.更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.5 JNI操作Java对象

JNI提供了Java和C/C++方法互操作的机制,上节只介绍了如何在Java中调用JNI实现方法,那JNI又是如何操作Java层呢?
JNI方法接受的第二个参数是Java对象:jobject,可以在JNI中操作这个jobject进而操作Java对象提供的变量和方法。

2.5.1 访问Java对象

要操作jobject,就是要访问这个对象并操作它的变量和方法。JNI提供的类和对象操作函数有很多,常用的有两个:FindClass和GetObjectClass,在C和C++中分别有不同的函数原型。

C++中的函数原型如下:

jclass FindClass(const char name);//查找类信息
jclass GetObjectClass(jobject obj);//返回对象的类
C中的函数原型如下:
jclass (FindClass)(JNIEnv, const char);
jclass (GetObjectClass)(JNIEnv, jobject);
我们可以看看Log系统是怎么操作Java对象的。打开android_util_Log.cpp,定位到register_android_util_Log函数:
int register_android_util_Log(JNIEnv env)
{
jclass clazz = env->FindClass("android/util/Log");
……
}
通过给FindClass传入要查找类的全限定类名(以“/”分隔路径)即可,之后方法返回一个jclass的对象,这样就可以操作这个类的方法和变量了。

2.5.2 操作成员变量(域)和方法

上节通过JNI提供的类操作函数得到了类的引用,通过这个引用便可以操作这个类上提供的方法和变量。JNI 用名字和类型签名来识别方法和域(变量)。

注意 Java中习惯将变量称为成员变量,而不是域。这里为了兼容JNI命名规则和Java习惯,将域和变量等价。

从名字和类型签名来操作对象上的域和方法可分为两步。还是以Log系统为例。打开android_util_Log.cpp,找到register_android_util_Log方法,代码如下:
int register_android_util_Log(JNIEnv env)
{

jclass clazz = env->FindClass("android/util/Log");
levels.debug = env->GetStaticIntField(clazz, 
               env->GetStaticFieldID(clazz, "DEBUG", "I"));
……

}

首先,通过FindClass方法找到android/util/Log的类信息clazz;然后,以clazz为参数调用GetStaticFieldID(clazz, "DEBUG", "I"),其中DEBUG是要访问的Java域的名字,I是该Java域的类型签名,即整型。GetStaticFieldID的函数原型如下:

jfieldID GetStaticFieldID(jclass clazz, const char name, const char sig)
该函数返回了一个jfieldID,代表Java成员变量。最后将该jfieldID传给GetStaticIntField方法,得到Java层的成员变量DEBUG的值,即3。

下面是Log.java的源码:
public final class Log {
……
public static final int DEBUG = 3;
……
}

JNI调用Java层的方法与此类似,流程是:
FindClass->GetMethodID返回(jmethodID)->CallMethod
这里仅提供函数列表,不再详细解释。

表2-4中列出了JNI提供的操作域和方法的函数。


image

2.5.3 全局引用、弱全局引用和局部引用

Java对象的生命周期由虚拟机管理,虚拟机内部维护一个对象的引用计数,如果一个对象的引用计数为0,这个对象将被垃圾回收器回收并释放内存。这里就有一个问题,如果Java对象中使用了Native方法,那会对对象的生命周期产生什么影响呢?

回答这个问题前,先看Log系统的例子。代码如下:

//static jobject clazz_ref1 = NULL; 方法1加入的code,见下文对方法1的解释
static jboolean android_util_Log_isLoggable(JNIEnv env, jobject clazz,

   jstring tag, jint level)

{

……
//clazz_ref1 = clazz;            方法1加入的code
//static  jobject clazz_ref2  = NULL;方法2加入的code
//clazz_ref2 = clazz;            方法2加入的code
if ((strlen(chars)+sizeof(LOG_NAMESPACE)) > PROPERTY_KEY_MAX) {
  ……//异常处理代码
} else {
    result = isLoggable(chars, level);
}
……

}

这部分代码中,并没有操作传进来的jobject对象,在这里对其进行修改,加入自己的代码保存传进来的jobject对象。要达到保存jobject对象的目的,C/C++程序员有两种方法:
方法1 在方法外加入全局变量,并在方法内赋值。

方法2 在方法内加入静态变量,并赋值。

这两种方法能达到我们的目的吗?

很不幸,答案是不能,而且后果很严重。

因为这样做,虚拟机无法跟踪该对象的引用计数,相当于没有增加引用计数。如果jobject已经被虚拟机回收,clazz_ref1和clazz_ref2将引用一个野指针,C/C++程序员应该知道野指针的问题有多严重。

那既然传统的方法无法保存对象,我们又该怎么做呢?

既然赋值操作无法通知虚拟机增加对象的引用计数,那是不是应该想到JNIEnv能替我们做些什么?因为到目前为止,我们能操作的只有这个接口。

幸运的是,JNIEnv已经为我们提供了解决方案:局部引用、全局引用和弱全局引用。
先来看JNI规范中是怎么定义这三种引用的。

1.局部引用

可以增加引用计数,作用范围为本线程,生命周期为一次Native调用。局部引用包括多数JNI函数创建的引用,Native方法返回值和参数。局部引用只在创建它的Native方法的线程中有效,并且只在Native方法的一次调用中有效,在该方法返回后,被虚拟机回收(不同于C中的局部变量,返回后会立即回收)。

2.全局引用

可以增加引用计数。作用范围为多线程,多个Native方法,生命周期到显式释放。全局引用通过JNI函数NewGlobalRef创建,并通过DeleteGlobalRef释放。如果程序员不显式释放,将永远不会被垃圾回收。

3.弱全局引用

不能增加引用计数。作用范围为多线程,多个Native方法,生命周期到显式释放。不过其对应的Java对象生命周期依然取决于虚拟机,意思是即便弱全局引用没有被释放,其引用的Java对象可能已经被释放。弱全局引用通过JNI函数NewWeakGlobalRef创建,并通过DeleteWeakGlobalRef释放。弱全局引用的优点是:既可以保存对象,又不会阻止该对象被回收。

注意 使用弱全局引用的时候,一定要注意:它所指向的对象可能已经被回收了。JNI 提供了IsSameObject函数用来判断弱引用对应的对象是否已经被回收,方法是用弱全局引用和NULL进行比较,如果返回JNI_TRUE,则说明弱全局引用指向的对象已经被回收。

IsSameObject的方法声明如下。
在C中:
jboolean (IsSameObject)(JNIEnv,jobject,jobject);
在C++中:
jboolean IsSameObject(jobject ref1, jobject ref2);
假设有一个弱引用weak_gref,可以按照如下方法使用:
if(env->IsSameObject(weak_gref,NULL) == JNI_TRUE)
{
//do something with weak_gref
}
既然已经知道了JNI中如何保存对象,我们继续修改代码,引入全局引用达到保存对象的目的。修改如下:
static jobject g_clazz_ref = NULL;
static jboolean android_util_Log_isLoggable(JNIEnv* env,

   jobject clazz, jstring tag, jint level)

{

……
g_clazz_ref = env->NewGlobalRef(clazz);
if ((strlen(chars)+sizeof(LOG_NAMESPACE)) > PROPERTY_KEY_MAX) {
 ……
} else {
   result = isLoggable(chars, level);
}

……
}
//一定要记住,在不使用该类的时候显式删除

env->DeleteGlobalRef(g_clazz_ref);

Android中对局部引用和全局引用的使用都有一定限制。如果引用超过一定数量,或者使用不当,非常容易引起内存不足和内存泄露问题。

对于全局引用,默认不能超过2000个,否则会出现内存不足的警告。如果在Dalvik的启动参数dalvik.vm.checkjni中设置打开checkjni的选项,Dalvik将监控全局引用的数量,如果超过2000, 在logcat中会看到“GREF overflow”,提示内存不足。GREF便是全局引用的缩写。

相关文章
|
3月前
|
Java Android开发 C++
Android Studio JNI 使用模板:c/cpp源文件的集成编译,快速上手
本文提供了一个Android Studio中JNI使用的模板,包括创建C/C++源文件、编辑CMakeLists.txt、编写JNI接口代码、配置build.gradle以及编译生成.so库的详细步骤,以帮助开发者快速上手Android平台的JNI开发和编译过程。
223 1
|
4月前
|
Java BI 数据处理
如何在Java中实现Excel操作
如何在Java中实现Excel操作
|
2月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
119 1
|
3月前
|
Java 调度 Android开发
Android经典实战之Kotlin的delay函数和Java中的Thread.sleep有什么不同?
本文介绍了 Kotlin 中的 `delay` 函数与 Java 中 `Thread.sleep` 方法的区别。两者均可暂停代码执行,但 `delay` 适用于协程,非阻塞且高效;`Thread.sleep` 则阻塞当前线程。理解这些差异有助于提高程序效率与可读性。
71 1
|
3月前
|
Android开发
Cannot create android app from an archive...containing both DEX and Java-bytecode content
Cannot create android app from an archive...containing both DEX and Java-bytecode content
35 2
|
4月前
|
存储 Java 索引
Java ArrayList操作指南:如何移除并返回第一个元素
通过上述方法,你可以方便地从Java的 `ArrayList` 中移除并返回第一个元素。这种操作在日常编程中非常常见,是处理列表时的基本技能之一。希望这篇指南能帮助你更好地理解和运用Java的 `ArrayList`。
47 4
|
4月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
【7月更文挑战第28天】在 Android 开发中, NDK 让 Java 与 C++ 混合编程成为可能, 从而提升应用性能。**为何选 NDK?** C++ 在执行效率与内存管理上优于 Java, 特别适合高性能需求场景。**环境搭建** 需 Android Studio 和 NDK, 工具如 CMake。**JNI** 构建 Java-C++ 交互, 通过声明 `native` 方法并在 C++ 中实现。**实战** 示例: 使用 C++ 计算斐波那契数列以提高效率。**总结** 混合编程增强性能, 但增加复杂性, 使用前需谨慎评估。
140 4
|
4月前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
61 3
|
4月前
|
分布式计算 DataWorks Java
DataWorks操作报错合集之使用ODPS Tunnel Upload功能时,遇到报错:Java 堆内存不足,该如何解决
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
4月前
|
SQL 缓存 Java
使用MyBatis优化Java持久层操作
使用MyBatis优化Java持久层操作
下一篇
无影云桌面