前言
上篇文章介绍了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!








网友评论