JNI访问Java对象的成员

3,868 阅读8分钟

经过前面的几篇文章的学习,我们已经打通了Java层到Native层的通道。Java层调用Native层很简单,只需要调用一个native方法,那么Native层如何回调Java层呢,从这篇文章开始,我们就来探讨这个问题。

Java类的成员(变量和方法)有静态和非静态之分,静态成员通常通过类来直接调用(当然也可以用对象调用),非静态成员通过类的对象来调用。在JNI层也遵从同样的规则。因此我将通过两篇文章分别来说明,而今天这篇文章就是讲述在JNI层如何通过Java对象来访问成员(变量和方法)。

准备工作

通过对前面文章的学习,我们已经学会了如何以最快的速度实现函数的"动态注册",那么我们来先做一个准备工作,实现"动态注册"

首先准备一个带有native方法的Java类,名为Person.java

package com.bxll.jnidemo;

public class Person {

    static {
        System.loadLibrary("person_jni");
    }

    native void native_init();
    
    private int mAge = -1;
    private String mName;

    public Person() {
        native_init();
    }

    public void setName(String name) {
        mName = name;
    }

    public String toString() {
        return "Name: " + mName + ", Age: " + mAge;
    }
    
    public static void main(String[] args) {
        System.out.println(new Person());
    }    
}

然后在Native层进行实现,实现的文件为person.cpp

#include <jni.h>
#include <iostream>

static void com_bxll_jnidemo_Person_init(JNIEnv * env, jobject obj)
{

}

const JNINativeMethod methods[] = 
{
    {"native_init", "()V", (void *)com_bxll_jnidemo_Person_init}
};

jint JNI_OnLoad(JavaVM * vm, void * reserved)
{
    int jni_version = -1;

    JNIEnv * env = NULL;
    if (vm->GetEnv((void **)&env, JNI_VERSION_1_6) == JNI_OK)
    {
	jclass clazz_person = env->FindClass("com/bxll/jnidemo/Person");
	if (env->RegisterNatives(clazz_person, methods, sizeof methods / sizeof methods[0]) == JNI_OK)
	{
	    jni_version = JNI_VERSION_1_6;
	}
    }

    return jni_version;
}

那么,com_bxll_jnidemo_Person_init函数就是Java层的native方法的实现,我们将在这个函数中来讲解如何访问Java对象的变量和方法。

访问对象的变量

com_bxll_jnidemo_Person_init函数中来实现获取Java对象变量的值,以及设置变量的值。

static void com_bxll_jnidemo_Person_init(JNIEnv * env, jobject obj)
{
    // 获取Java对象obj的Class对象
    jclass clazz_person = env->GetObjectClass(obj);
    // 从Class对象中获取mAge变量
    jfieldID fieldID_mAge = env->GetFieldID(clazz_person, "mAge", "I");
    // 从Java对象obj中获取变量mAge的值
    jint age = env->GetIntField(obj, fieldID_mAge);
    std::cout << "Age: " << age << std::endl;
    // 设置Java对象obj的变量mAge的值
    env->SetIntField(obj, fieldID_mAge, 18);
}

从注释中可以看出,JNI层获取Java对象的某个变量值以及设置变量值的方式,是不是和使用Java反射的方式非常像。

如果你懂Java反射,那么这里的逻辑就顺理成章,当然,如果你不懂也没关系,只要我们记住获取对象变量值最终调用的函数是什么,在这里就是GetIntField,然后通过参数一步一步推断就可以了。

那么现在,我们来看看这里涉及的函数

获取jclass对象

jclass GetObjectClass(JNIEnv *env, jobject obj);

jobject obj代表一个Java对象,函数返回一个对象的Class对象。

通过JNI函数动态注册之JNI类型和签名这篇文章可知,jclassJavaClass对象在JNI中的类型。

Java反射一样,获取到了Class对象后就可以从中获取变量和方法。

获取jfieldID

jfieldID GetFieldID(JNIEnv *env, jclass clazz, 
                const char *name, const char *sig);

参数

  1. env: 指向JNIEnv的指针。
  2. clazz: Class对象,可以通过GetObjectClass()或者FindClass()获取。
  3. name: Class对象的某个变量的名字。
  4. sig: Class对象的变量的类型签名。

如果不知道参数sig的类型签名如何写,可以阅读JNI函数动态注册之JNI类型和签名进行了解。

返回一个Class对象的变量,类型为jfieldIDClass对象的变量由参数namesig唯一标识。

Java反射一样,获取到了变量后,就可以通过这个变量获取到变量的值,也可以设置变量的值。

获取变量的值

根据变量的类型,会有不同的函数来获取变量的值。函数的基本形式如下

NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
基本类型函数名 返回类型
GetBooleanField() jboolean
GetByteField() jbyte
GetCharField() jchar
GetShortField() jshort
GetIntField() jint
GetLongField() jlong
GetFloatField() jfloat
GetDoubleField() jdouble
GetObjectField() jobject

前八项对应于Java的八中基本类型,第九项对应于Java的所有引用类型。

例如对于int类型,对应的函数原型你如下

jint GetIntField(JNIEnv * env, jobject obj, jfieldID fieldID);

参数

  • jobject obj代表一个Java对象
  • jfieldID fieldID代表Class对象的某个变量,通过GetFieldID()获取。

设置变量的值

根据变量的类型,也会有不同的函数来设置变量的值。函数的基本形式如下

void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID,
                    NativeType value);
函数名 参数类型
SetBooleanField() jboolean
SetByteField() jbyte
SetCharField() jchar
SetShortField() jshort
SetIntField() jint
SetLongField() jlong
SetFloatField() jfloat
SetDoubleField() jdouble
SetObjectField() jobject

前八项对应于Java的八中基本类型,第九项对应于Java的所有引用类型。

例如,对与int类型,函数原型如下

void setIntField(JNIEnv * env, jobject obj, jfieldID fieldID, jint value);

参数

  • jobject obj代表Java对象
  • jfieldID fieldID代表Class对象的某个方法,通过GetFieldID()获取
  • jint value代表要设置的int

访问对象的方法

我们仍然在com_bxll_jnidemo_Person_init函数中来调用Java对象的方法

static void com_uni_ndkdemo_Person_init(JNIEnv *env, jobject obj) {
    // 获取Class对象
    jclass clazz_person = env->GetObjectClass(obj);
    // 从Class对象中获取setName方法
    jmethodID methodID_setName = env->GetMethodID(clazz_person, "setName", "(Ljava/lang/String;)V");
    // 创建调用setName()需要的参数值
    jstring name = env->NewStringUTF("David");
    // 调用Java对象obj的setName()方法
    env->CallVoidMethod(obj, methodID_setName, name);
}

我们同样可以发现,在JNI中调用Java对象的方法,和Java反射使用的方式几乎一样。我们现在来看看涉及到的JNI函数的作用。

获取jmethodID

jmethodID GetMethodID(JNIEnv *env, jclass clazz,
                        const char *name, const char *sig);

参数

  • jclass clazz: Java类的Class对象,需要通过GetObjectClass()或者FindClass()获取。
  • const char * name: 方法名,需要通过GetMethodID()获取。
  • const char * sig: 方法签名。

如果不知道参数sig的方法签名如何写,可以阅读JNI函数动态注册之JNI类型和签名进行了解。

返回值

  • 如果找到了对应的方法,就会返回一个jmethodID对象,否则返回NULL

注意: 如果要获取Java类的构造方法,参数const char *name的值为<init>,参数const char *sig的值为void (V)。这个是比较特殊的,需要注意。

调用对象的方法

根据Java方法返回的类型的不同,JNI有不同的函数来调用Java对象的方法。不过基本形式有三种

NativeType Call<type>Method(JNIEnv *env, jobject obj,
jmethodID methodID, ...);

NativeType Call<type>MethodA(JNIEnv *env, jobject obj,
jmethodID methodID, const jvalue *args);

NativeType Call<type>MethodV(JNIEnv *env, jobject obj,
jmethodID methodID, va_list args);

可以看到这三类函数的区别在于传参的方式不同。

第一个函数和第三个函数其实原理都是一样的,不过最常用的应该就是第一个。

那么第二个函数就有点意思了,参数使用的是jvalue类型的数组作为参数。

typedef union jvalue {
    jboolean    z;
    jbyte       b;
    jchar       c;
    jshort      s;
    jint        i;
    jlong       j;
    jfloat      f;
    jdouble     d;
    jobject     l;
} jvalue;

jvalue是一个联合体,联合体中定义Java基本类型和引用类型的变量,因此就可以以数组的行为来传递参数,这下就明白了吧。

我们以第一个函数为例来说明下具体的函数原型。

如果一个Java方法返回的是基本类型,例如int,那么函数原型如下

jint CallIntMethod(JNIEnv * env, jobject obj, jmethodID methodID, ...);

而如果一个Java方法的返回类型是引用类型,例如String,那么函数原型如下

jobject CallObjectMethod(JNIEnv * env, jobject obj, jmethodID methodID, ...);

当然,如果一个Java方法的返回类型为void,那么函数原型如下

void CallVoidMethod(JNIEnv * env, jobject obj, jmethodID methodID, ...);

参数

  • jobject obj: 代表Java对象
  • jmethodID methodID: 对象的方法,通过GetMethodID()获取
  • ...: 可变参数,代表需要传入给Java方法的参数。

完整的实现

static void com_bxll_jnidemo_Person_init(JNIEnv * env, jobject obj)
{
    jclass clazz_person = env->GetObjectClass(obj);
    jfieldID fieldID_mAge = env->GetFieldID(clazz_person, "mAge", "I");
    // 判断是否能获取到变量
    if (fieldID_mAge == NULL)
    {
        std::cout << "Can't find Person.mAge.'" << std::endl;
        return;
    }
    jint age = env->GetIntField(obj, fieldID_mAge);
    std::cout << "Age: " << age << std::endl;
    env->SetIntField(obj, fieldID_mAge, 18);


    jmethodID methodID_setName = env->GetMethodID(clazz_person, "setName", "(Ljava/lang/String;)V");
    // 判断是否能获取到方法
    if (methodID_setName == NULL)
    {
        std::cout << "Can't find setName method." << std::endl;
        return;
    }
    jstring name = env->NewStringUTF("David");
    env->CallVoidMethod(obj, methodID_setName, name);

    // 手动释放局部引用
    env->DeleteLocalRef(name);
    env->DeleteLocalRef(clazz_person);
}

在这个完整的实现中,我们增加了判空,以及手动释放局部引用,这是一个比较好的JNI开发习惯。

总结

通过调用Java对象的native方法,虚拟机会调用相应的本地函数,而本地函数的第二个参数一定为jobject object,代表的就是Java对象。于是可以通过JNI技术,访问这个对象的成员(变量和方法)。

在下一篇文章,我们就来讲解如何在JNI层访问类的静态成员(变量和方法)。如果你理解了本文的内容,那么下一篇文章就只是一个拓展。

参考

docs.oracle.com/javase/7/do…