前言
上篇文章介绍了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上面的限定符如
public
、private
等将会被忽略。
字段描述符
Java类型 | 描述符 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
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上面的限定符如
public
、private
等将会被忽略。
调用父类方法
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/native
比java/java
要慢,业界估计是java/native
是java/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倍。
结语
End!
网友评论