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

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提供的操作域和方法的函数。

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便是全局引用的缩写。

时间: 2024-07-30 09:55:27

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

《Android的设计与实现:卷I》——第2章 2.2.3Log系统的JNI方法注册

2.2.3 Log系统的JNI方法注册 JNI层已经实现了Java层声明的Native方法.可这两个方法又是如何联系在一起的呢?我们接着分析android_util_Log.cpp的源码.定位到以下部分: static JNINativeMethod gMethods[] = { { "isLoggable", "(Ljava/lang/String;I)Z", (void) android_util_Log_isLoggable }, { "printl

《Android的设计与实现:卷I》——第2章 2.3 JNI总管:JNIEnv

2.3 JNI总管:JNIEnv 在Log系统的实例中,JNI层实现方法和注册方法中都使用了JNIEnv这个指针,通过它调用JNI函数,访问Java虚拟机,进而操作Java对象.JNIEnv是JNI编程中最重要的概念,本节将详细介绍它.首先看JNIEnv的体系结构,如图2-2所示. 在图2-2中可以看到,JNIEnv首先指向一个线程相关的结构,该结构又指向一个指针数组,在这个指针数组中的每个元素最终指向一个JNI函数.所以可以通过JNIEnv去调用JNI函数. 打开jni.h文件看看这部分内容是

《Android的设计与实现:卷I》——第2章 框架基础JNI

第2章 框架基础JNI JNI(Java Native Interface,Java本地接口)是Java平台上定义的一套标准的本地编程接口.JNI允许Java代码与本地代码互操作,即Java代码可以调用本地代码,本地代码也可以调用Java代码.所谓本地代码指的是用其他编程语言(如C/C++)实现的.依赖于特定硬件和操作系统的代码.通过JNI调用本地代码,可以实现Java语言所不能实现的功能.在Android平台上,Dalvik虚拟机会实现JNI定义的接口. 2.1 JNI在Android系统中所

《Android的设计与实现:卷I》——第3章 Android启动过程的底层实现

第3章 Android启动过程的底层实现 Android支持多种启动模式,主要有正常模式(normal mode).安全模式(safe mode).恢复模式(recovery mode).工厂模式(factory mode).快速启动模式(fastboot mode)等.除正常模式外,都是刷机或者测试模式,本书只讲解正常模式下Android的启动过程.如果读者对其他启动模式感兴趣,可以自行查阅相关资料. 3.1 Android正常模式启动流程 Android的正常模式启动流程大体如下:步骤1 系

Android UI设计的幻灯片:新的UI设计模式

文章描述:谷歌Android UI设计技巧:新的UI设计模式. 本系列文章原是Android的官方开发者博客的一份Android UI设计的幻灯片,51CTO的译者将这份教程5部分进行翻译整理,希望对Android开发者能有帮助.本文为<谷歌Android UI设计技巧>第四部分:新的UI设计模式. 本文为<谷歌Android UI设计技巧>第四部分:新的UI设计模式. [1] [2]  下一页

Android UI设计的幻灯片:图标与指导说明

文章描述:谷歌Android UI设计技巧:图标与指导说明. 本系列文章原是Android的官方开发者博客的一份Android UI设计的幻灯片,51CTO的译者将这份教程5部分进行翻译整理,希望对Android开发者能有帮助.本文为<谷歌Android UI设计技巧>第五部分也就是最后一部分:图标与指导说明. 本文为<谷歌Android UI设计技巧>第五部分也就是最后一部分:图标与指导说明.

Android应用设计:选项菜单Options Menu

文章描述:Android硬体键交互之"选项菜单". 众所周知Android没有明确的GuideLine,虽说没有严格的规范来限制设计与创新很赞,但这也导致市场上的Android应用设计上的混乱.一个典型例子就是选项菜单Options Menu. 混乱的菜单 Android机器采用的硬体键来呼出菜单,这种方式在表现上隐性的,用户对于何种情况下可以呼出何种菜单没有预见性,甚至是否可以呼出菜单都没有预期.   如何解决 为降低用户的认知成本,建议设计中遵循以下方式. Question 1:何

谷歌Android UI设计技巧:新的UI设计模式

本系列文章原是Android的官方开发者博客的一份Android UI设计的幻灯片,51CTO的译者将这份教程5部分进行翻译整理,希望对Android开发者能有帮助.本文为<谷歌Android UI设计技巧>第四部分:新的UI设计模式. 本文为<谷歌Android UI设计技巧>第四部分:新的UI设计模式.

谷歌Android UI设计技巧:框架特性

本系列文章原是Android的官方开发者博客的一份Android UI设计的幻灯片,51CTO的译者将这份教程5部分进行翻译整理,希望对Android开发者能有帮助.本文为<谷歌Android UI设计技巧>第三部分:框架特性. 本文为<谷歌Android UI设计技巧>第三部分:框架特性. 注:相对布局和线性布局是Android里面常用的两种布局,线性布局比较简单,而相对布局可以做出比较复杂的布局管理,所以仅仅了解线性布局,很多时候是不够的.不过以作者之前Qt的经验来看,Andr