JNI编程指南(二):字段和方法

前言

上篇文章介绍了JNI中访问JVM中任意基本类型数据和字符串、数组这样的引用类型,这篇就简单介绍下JNI对JVM中任意对象的字段和方法进行交互,简单点说就是本地代码中调用Java的代码,也就是通常所说的来自本地方法的callback(回调)。

访问字段

Java层代码:

package com.net168.xxx
class Simple {
    private String str;       //实例字符串变量
    public int num;           //实例整型变量
	private int static count; //静态整型变量
    
    private native void test();
}

对应native的代码实现:

JNIEXPORT void JNICALL
    Java_Com_net168_xxx_Simple_test(JNIEnv *env, jobject thiz)
{
    jfieldID fid;
    //获取Simple类的字节码
    jclass cls = env->GetObjectClass(thiz);
    
    //获取str这个String的字段ID  ---  操作 实例字符串变量
    fid = env->GetFieldID(cls, "str", "Ljava/lang/String;");
    //获取thiz实例的str字段值jstring
    jstring jstr = static_cast<jstring>(env->GetObjectField(thiz, fid));
   
    //获取num这个int的字段ID  ---  操作 实例整型变量
    fid = env->GetFieldID(cls, "num", "I");
    //设置thiz实例的num字段值为10086
    env->SetIntField(thiz, fid, 10086);
	
	//获取count这个静态int字段ID  ---  操作 静态整型变量
    fid = env->GetStaticFieldID(cls, "count", "I");
    //获取Simple类的num这静态变量的值
    env->GetStaticIntField(cls, fid);
}

知识点

  • 类引用(类字节码jclass)获取可以通过GetObjectClass(jobject obj),但是前提需要有一个jobect实例的引用,也可以通过FindClass(const char* name)传入类的相关路径信息获取相应jclass数据。
  • 获取实例变量的字段ID的方法原型是GetFieldID(jclass clazz, const char* name, const char* sig)
    获取静态变量的字段ID的方法原型是GetStaticFieldID(jclass clazz, const char* name, const char* sig);
    clazz指的是要获取类的引用,name则是获取字段的名字,sig代表对应字段的描述符。
  • 对于实例变量,我们可以通过Get/Set<Type>Field(jobject,jfieldID)这个方法来进行变量的获取和设置,
    对于静态变量,我们可以通过Get/SetStatic<Type>Field(jclass,jfieldID)这个方法来进行变量的获取和设置;
    值得注意的是实例变量传入的是jobect引用实例,而静态变量传入的是jclass字节码数据。
  • 对于字段ID,在Java上面的限定符如publicprivate等将会被忽略。

字段描述符

Java类型描述符
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
voidV
object对象以"L"开头,以";“结尾,中间是用”/"隔开的包及类名;比如:Ljava/lang/String;
嵌套内部类嵌套类,则用$来表示嵌套;比如:Landroid/os/FileUtils$FileStatus;
数组类型数组类型则用"["加上如表所示的对应类型;例如:[L/java/lang/objects;

调用方法

Java层代码:

package com.net168.xxx
class Simple {
    public void functionA() {  //无入参,无返回值函数

    }
    private String functionB(int num) {  //入参int,返回字符串
        return "";
    }
    protected int functionC(String str, int num) { //入参String和int,返回整型
        return 0;
    }
    public static int functionD() {  //静态函数,无入参,返回整型数值
        return 0;
    }

    private native void test();
}

对应native的代码实现:

JNIEXPORT void JNICALL
    Java_Com_net168_xxx_Simple_test(JNIEnv *env, jobject thiz)
{
    jmethodID mid;
    //获取Simple类的字节码
    jclass cls = env->GetObjectClass(thiz);

    //获取实例函数functionA()的ID
    mid = env->GetMethodID(cls, "functionA", "()V");
    //调用Java函数public void functionA(),无入参,无返回值
    env->CallVoidMethod(thiz, mid);

    //获取实例函数functionB()的ID
    mid = env->GetMethodID(cls, "functionB", "(I)Ljava/lang/String;");
    //调用Java函数private String functionB(int num),传入0,返回字符串
    jstring str = static_cast<jstring>(env->CallObjectMethod(thiz, mid, 0));

    //获取实例函数functionC()的ID
    mid = env->GetMethodID(cls, "functionC", "(Ljava/lang/String;I)I");
    //调用Java函数protected int functionC(String str, int num),传入str和0,返回int
    jint num = env->CallIntMethod(thiz, mid, str, 1);

    //获取静态函数functionD()的ID
    mid = env->GetStaticMethodID(cls, "functionD", "()I");
    //调用Java函数public static int functionD(),无入参,返回int
    env->CallStaticIntMethod(cls, mid);
}

知识点

  • 获取实例方法的原型是GetMethodID(jclass clazz, const char* name, const char* sig)
    获取静态方法的原型是GetStaticMethodID(jclass clazz, const char* name, const char* sig);
    clazz指的是要获取类的引用,name则是获取方法的名字,sig代表对应方法的描述符。
  • 方法描述符由(参数列表)返回值构成,参数类型出现在前面并由一对圆括号包围起来,参数类型按照他们在方法声明中出现的顺序被列出来,并且多个参数类型之间没有分隔符;如果一个方法没有参数则表示为一对空圆括号;方法返回值类型紧跟参数类型的右括号后面。
    例如(I)V表示这个方法的一个参数类型是int,并且有一个void类型返回值;()Ljava/lang/String;表示这个方法没有参数,其返回值是String类型。
  • 调用GetMethodID/GetStaticMethodID后,函数会在指定的类中寻找对应的方法,这个寻找过程基于方法的描述符,如果方法不存在,则其会返回NULL;并且立即从本地方法返回同时抛出一个NoSuchMethodError的错误。
  • 调用实例方法可以使用Call<Type>Method(jobect obj, jmethodID mid, ...),
    调用静态方法则调用CallStatic<Type>Method(jclass clz, jmethodID mid, ...);
    <Type>是对应的方法返回的类型,例如调用Void返回类型的则是CallVoidMethod(),值得注意的是实例方法传入的是实例引用jobect,而调用静态方法的则是jclass类字节码引用。
  • 对于方法ID,在Java上面的限定符如publicprivate等将会被忽略。

调用父类方法

Java层代码:

package com.net168.xxx
class Parent {
    public int function() {
        return 0;
    }
}
class Child extends Parent {
    @Override
    public int function() {
        return 1;
    }
    private native void test();
}

对应native的代码实现:

JNIEXPORT void JNICALL
    Java_Com_net168_xxx_Child_test(JNIEnv *env, jobject thiz)
{
	//获取子类jclass
    jclass cls1 = env->GetObjectClass(thiz);
	//获取父类jclass
    jclass cls2 = env->FindClass("com/net168/xxx/Parent"); 
    
	//获取子类的function方法ID
    jmethodID mid1 = env->GetMethodID(cls1, "function", "()I");
	//获取父类的function方法ID
    jmethodID mid2 = env->GetMethodID(cls2, "function", "()I");
    //mid1 与 mid2 的ID值是不相等的
	
	env->CallVoidMethod(thiz, mid1);    //调用子类
    env->CallVoidMethod(thiz, mid2);    //调用子类
	
    env->CallNonvirtualIntMethod(thiz, cls1, mid1);   //调用子类
    env->CallNonvirtualIntMethod(thiz, cls1, mid2);   //调用父类
	env->CallNonvirtualIntMethod(thiz, cls2, mid1);   //调用子类
    env->CallNonvirtualIntMethod(thiz, cls2, mid2);   //调用父类
}

知识点

  • 在子类和父类jclass通过GetMethodID获取的jmethodID是不一致的。
  • 调用父类方法可以通过调用CallNonvirtual<Type>Method(jobject obj, jclass clazz, jmethodID methodID, ...)实现;如果jmethodID是父类的方法ID,则无论传入jclass的类型都是调用到父类的方法,如果jmethodID是子类的方法ID,那么只有在jclass是父类的字节码才会调用父类方法。
  • 调用父类实例方法的情况较少,因为可以在Java层简单通过super.函数名()实现。

调用构造函数

Java层代码:

package com.net168.xxx
class Simple {
	public Simple(int num) {
	
	}
	private native static void test();
}

对应的native实现:

JNIEXPORT void JNICALL
    Java_Com_net168_xxx_Simple_test(JNIEnv *env, jclass clz)
{
	//获取字节码jclass
    jclass cls = env->FindClass("com/net168/xxx/Simple");
    //获取Simple的构造函数,入参是int
    jmethodID mid = env->GetMethodID(cls, "<init>", "(I)V");

	//以下是两种构造实例引用的方法
	
    //创建一个Simple的实例    方法一
    jobject obj1 = env->NewObject(cls, mid, 1);

    //申请一个Simple的内存,但并没触发构造方法    方法二
    jobject obj2 = env->AllocObject(cls);
    //调用obj2的构造方法
    env->CallNonvirtualVoidMethod(obj2, cls, mid, 1);
}

知识点

  • 构造函数ID的获取,传入<init>作为方法名,V作为返回值,()的根据构造函数的入参决定。
  • 函数原型jobject NewObject(jclass clazz, jmethodID methodID, ...)通过传入构造函数的jclass和构造函数以及入参,返回新建的实例引用jobect
  • 可以通过AllocObject(jclass)创建一个未初始化的对象,然后通过调用CallNonvirtualVoidMethod()函数来调用构造方法,但是需要小心确保构造函数最多只能被调用一次。

字段ID缓存技术

使用时缓存
java代码:

package com.net168.xxx
class Simple {
	public Simple() {
	}
    public int num;
    
    private native void test();
}

对应的native实现:

JNIEXPORT void JNICALL
    Java_Com_net168_xxx_Simple_test(JNIEnv *env, jobject thiz)
{
	//申请一个静态变量
	static jfieldID s_fid = NULL;
	//懒加载,如果尚未获取ID则需要GetFieldID获取
    if (s_fid == NULL)
    {
        jclass cls = env->GetObjectClass(thiz);
        s_fid = env->GetFieldID(cls, "num", "I");
    }
	//获取字段ID所对应的数值
    jint num = env->GetIntField(thiz, fid);
}

静态初始化过程缓存
java代码:

package com.net168.xxx
class Simple {
	public Simple() {
		//构造函数调用ID获取的native方法
		initIDs();
	}
    public int num;
    
	private native void initIDs();
    private native void test();
}

对应的native实现:

jfieldID fid;
JNIEXPORT void JNICALL
    Java_Com_net168_xxx_Simple_initIDs(JNIEnv *env, jobject thiz)
{
    //获取字段ID
	jclass cls = env->GetObjectClass(thiz);
	fid = env->GetFieldID(cls, "num", "I");
}

JNIEXPORT void JNICALL
    Java_Com_net168_xxx_Simple_test(JNIEnv *env, jobject thiz)
{
    //由于构造函数已经获取了fid,这里可以直接使用
	jint num = env->GetIntField(thiz, fid);
}

知识点

  • 使用时缓存ID是在当改字段/方法ID被首次使用时缓存起来,提供后续的使用而不用重新获取该ID,但是每次使用都需要检查一下。
  • 静态初始化过程缓存ID是在构造函数时调用native方法获取相关字段/方法的ID值。
  • 当类被unload的时候,相对应的ID会失效,如果使用时缓存ID的话则需要确保这个类不会被unload,而静态初始化过程缓存ID则不用考虑这个问题,因为当类被unload和reload时,ID会被重新计算。
  • 当程序不能控制方法/字段所在类的源码时,使用时缓存ID是个合理的方案;反之建议在静态初始化时缓存字段/方法ID。
  • 不同线程获取同一个字段/方法ID是相同的,所以多线程调用不会导致混乱。

JNI操作Java字段和方法效率

知识点

  • JNI访问Java字段和方法的效率依赖于VM的实现,一般来说java/nativejava/java要慢,业界估计是java/nativejava/java调用时消耗的2到3倍,但是VM可以通过调整使得java/native的消耗接近或者等于java/java的消耗。
  • java/native在调用时将控制权和入口切换给本地方法之前,VM需要做一些额外的操作来创建参数和栈帧;并且java/java内联比较容易,而内联java/native方法要麻烦的多。
  • native/java调用理论上跟java/native调用时类似的,但是一般VM不会对此进行优化,多数VM中native/java调用消耗可以达到java/java调用的10倍。

结语

本文同步发布于简书CSDN

End!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值