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