JNI程序规范和指南4——字段和方法

这是一个关于JNI的系列文章。

上一篇文章介绍了JNI如何访问基本类型和引用类型数据,本文将继续介绍如何访问任意对象的字段和方法。在本地代码中调用Java中实现的方法,也就是常说的回调函数callback
本文会介绍如何使用JNI函数访问对象的字段和调用回调函数,后面也会介绍如何使用缓存来使得对象操作更加简便和有效率。在文章的最后,还会讨论以下Java调用C/C++方法,C/C++访问Java对象字段和调用callback的性能。

访问字段

Java支持两种字段,对象字段(instance fields)和静态字段(static fields)。

The JNI provides functions that native code can use to get and set instance fields in objects and static fields in classes.

让我们看一个简单的例子(访问对象字段):

import java.lang.*;
class InstanceFieldAccess {    
    private String s;
    private native void accessField();    
    public static void main(String args[]) {        
        InstanceFieldAccess c = new InstanceFieldAccess();       
         c.s = "abc";        
         c.accessField();        
         System.out.println("In Java:");        
         System.out.println("  c.s = \"" + c.s + "\"");    
    }    
    static {        
        System.loadLibrary("InstanceFieldAccess");    
    } 
}

InstanceFieldAccess类定义了对象的字段smain函数则创建了一个对象,为字段赋值,然后调用InstanceFieldAccess.accessField。该本地方法输出并修改该对象字段的值:

#include <jni.h>
#include <stdio.h>
#include "InstanceFieldAccess.h"

JNIEXPORT void JNICALL 
Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj) {    
    jfieldID fid;   
    /* store the field ID */    
    jstring jstr;    
    const char *str;
    /* Get a reference to obj’s class */    
    jclass cls = (*env)->GetObjectClass(env, obj);
    printf("In C:\n");
    /* Look for the instance field s in cls */    
    fid = (*env)->GetFieldID(env, cls, "s",  "Ljava/lang/String;");    
    if (fid == NULL) {        
        return; 
        /* failed to find the field */    
    }
    /* Read the instance field s */    
    jstr = (*env)->GetObjectField(env, obj, fid);    
    str = (*env)->GetStringUTFChars(env, jstr, NULL);    
    if (str == NULL) {        
        return; 
        /* out of memory */    
    }    
    printf("  c.s = \"%s\"\n", str);    
    (*env)->ReleaseStringUTFChars(env, jstr, str);
    /* Create a new string and overwrite the instance field */    
    jstr = (*env)->NewStringUTF(env, "123");    
    if (jstr == NULL) {        
        return; 
        /* out of memory */    
    }    
    (*env)->SetObjectField(env, obj, fid, jstr); 
}

运行结果如下:

In C:
  c.s = "abc"
In Java:
  c.s = "123"

访问对象对象字段的流程

本地方法访问对象的字段有两个步骤。首先,调用GetFieldID获取字段的ID:

fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");

其中你需要通过GetObjectClass来获取jobject obj的类引用cls, 当你获得了字段的ID之后,就可以通过适当的JNI函数获得字段的值, 比如:

jstr = (*env)->GetObjectField(env, obj, fid);

由于字符串和数组都属于对象,所以可以使用GetObjectField来访问字段。除此之外,JNI还支持GetIntFieldSetFloatField

字段描述符(field descriptors)

还记得前文说到"Ljava/lang/String;"来表示Java中的字段类型,这就是字段描述符,是由字段的声明类型决定的。比如"I"表示int,"F"表示float, "D"表示double, "Z"表示double等等。
引用类型的描述符: 以L开头,接着是Java中的类型名(.换成/,最后;结尾)。
数组类型的描述符:以[开头接着是元素的类型描述符。比如[I表示int []
可以使用javap工具获取.class文件中所有的类型描述符:

javap -s -p InstanceFieldAccess

访问静态字段

访问静态字段的方法和对象字段相似,看个例子:

import java.lang.*;
class StaticFieldAccess {    
    private static int si;
    private native void accessField();   
     public static void main(String args[]) {        
         StaticFieldAccess c = new StaticFieldAccess();        
         StaticFieldAccess.si = 100;        
         c.accessField();        
         System.out.println("In Java:");       
          System.out.println("  StaticFieldAccess.si = " + si);    
    }    
    static {        
        System.loadLibrary("StaticFieldAccess");    
    } 
}

StaticFieldAccess包含了一个静态字段int si。在StaticFieldAccess.main中初始化了静态字段然后调用StaticFieldAccess.accessField输出并修改字段:

#include <jni.h>
#include <stdio.h>
#include "StaticFieldAccess.h"

JNIEXPORT void JNICALL 
Java_StaticFieldAccess_accessField(JNIEnv *env, jobject obj) {    
    jfieldID fid;   
    /* store the field ID */    
    jint si;
    /* Get a reference to obj’s class */   
     jclass cls = (*env)->GetObjectClass(env, obj);
    printf("In C:\n");
    /* Look for the static field si in cls */    
    fid = (*env)->GetStaticFieldID(env, cls, "si", "I");    
    if (fid == NULL) {        
        return; 
        /* field not found */    
    }    
    /* Access the static field si */    
    si = (*env)->GetStaticIntField(env, cls, fid);    
    printf("  StaticFieldAccess.si = %d\n", si);    
    (*env)->SetStaticIntField(env, cls, fid, 200); 
}

输出结果:

In C:
  StaticFieldAccess.si = 100
In Java:
  StaticFieldAccess.si = 200

总结一下,访问静态字段和对象字段主要有两点差异:

  • 获取字段ID的方法不同:GetStaticFieldIDGetFieldID
  • 在访问字段的方法中(GetStaticIntField, GetStaticIntField),静态字段传递的是类引用(class reference)而对象方法传递的是对象引用(object reference)

调用函数

Java有几种函数类型,对象函数(Instance methods), 静态方函数(static methods)和构造函数(constructors)。
JNI支持很多方法来调用Java的回调函数。下面是一个C调用Java函数的例子:

import java.lang.*;

class InstanceMethodCall {    
    private native void nativeMethod();    
    private void callback() {        
        System.out.println("In Java");    
    }    
    public static void main(String args[]) {        
        InstanceMethodCall c = new InstanceMethodCall();        
        c.nativeMethod();    
    }    
    static {       
        System.loadLibrary("InstanceMethodCall");    
    } 
}

本地代码实现:

#include <jni.h>
#include <stdio.h>
#include "InstanceMethodCall.h"

JNIEXPORT void JNICALL 
Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj) {    
    jclass cls = (*env)->GetObjectClass(env, obj);   
    jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "()V");    
    if (mid == NULL) {        
        return; 
        /* method not found */    
    }    
    printf("In C\n");    
    (*env)->CallVoidMethod(env, obj, mid); 
}

运行结果:

In C
In Java

调用实例方法

Java_InstanceMethodCall_nativeMethod函数展示了如何调用一个对象的函数:

  • 首先调用GetMwthodID遍历给定类的方法(根据返回类型和名字), 如果该方法不存在,则返回NULL并抛出NoSuchMethodError异常。
  • 然后调用CallVoidMethod去执行该对象的方法。

除了CallVoidMethod,JNI还提供了其他的函数去执行Java中定义的函数。比如CallIntMethod, CallObjectMethod等等。另外还可以使用Call<Type>Method这类方法去调用Java中的API。

jobject thd = ...; 
/* a java.lang.Thread instance */ 
jmethodID mid; 
jclass runnableIntf =    (*env)->FindClass(env, "java/lang/Runnable"); 
if (runnableIntf == NULL) {    
  ... 
  /* error handling */ 
} 
mid = (*env)->GetMethodID(env, runnableIntf, "run", "()V"); 
if (mid == NULL) {    
  ... 
  /* error handling */ 
} 
(*env)->CallVoidMethod(env, thd, mid); 
... 
/* check for possible exceptions */

生成方法描述符

JNI使用类似与定义字段类型的描述符来定义函数的类型。一个函数描述符包含了形参类型和返回类型,形参类型在前并使用()括住,遵循函数中的排列顺序,并且多个形参之间没有分隔符。返回类型描述符紧跟其后。同样可以使用javap工具生成描述符。

调用静态方法

根据以下步骤在本地代码中调用Java定义的静态函数:

  • 使用GetStaticMethodID获取函数的ID
  • 将类,方法ID和参数传给CallStatic<Type>Method

注意,调用静态方法传递的是类引用而实例方法则是传递对象的引用。在Java中你可以通过类直接调用静态方法,也可以通过一个new对象来调用(类Cls中有静态方法f,Cls.fobj=new Cls();obj.f都是合法的)。但是在JNI中,本地方法只能通过类引用来调用静态方法。看个例子:
Java代码:

import java.lang.*;

class StaticMethodCall {    
    private native void nativeMethod();    
    private static void callback() {        
        System.out.println("In Java");    
    }    
    public static void main(String args[]) {        
        StaticMethodCall c = new StaticMethodCall();        
        c.nativeMethod();    
    }    
    static {        
        System.loadLibrary("StaticMethodCall");    
    } 
}

C代码:

#include <jni.h>
#include <stdio.h>
#include "StaticMethodCall.h"

JNIEXPORT void JNICALL 
Java_StaticMethodCall_nativeMethod(JNIEnv *env, jobject obj) {    
    jclass cls = (*env)->GetObjectClass(env, obj);    
    jmethodID mid = (*env)->GetStaticMethodID(env, cls, "callback", "()V");    
    if (mid == NULL) {        
        return;  
        /* method not found */    
    }    
    printf("In C\n");    
    (*env)->CallStaticVoidMethod(env, cls, mid); 
}

调用父类的实例方法

你可以调用已经被重载过的父类的实例方法。JNI提供了CallNonvirtual<Type>Method这类方法。你需要按照以下的步骤进行调用:

  • 调用GetMethodID从一个指向父类的引用中获取函数ID
  • 传递对象实例(object),父类引用(superclass),函数ID和参数给CallNonvirtual<Type>Method

本地代码调用父类的实例方法是很少见到,Java中实现就很简单: super.f()
CallNonvirtualVoidMethod还可以用在调用父类构造函数上。

调用构造函数

JNI中调用构造函数的过程和调用实例方法很相似。为了获取构造函数的ID,需要传入<init>作为函数名,V作为函数返回类型。然后就可以通过NewObject等函数调用构造函数。以下例子在C中构造一个java.lang.String对象:

jstring 
MyNewString(JNIEnv *env, jchar *chars, jint len) {    
  jclass stringClass;    
  jmethodID cid;    
  jcharArray elemArr;    
  jstring result;
  stringClass = (*env)->FindClass(env, "java/lang/String");    
  if (stringClass == NULL) {        
    return NULL; 
    /* exception thrown */ 
  }
  /* Get the method ID for the String(char[]) constructor */ 
  cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V");  
  if (cid == NULL) {        
    return NULL; 
    /* exception thrown */    
  }
  /* Create a char[] that holds the string characters */ 
  elemArr = (*env)->NewCharArray(env, len);    
  if (elemArr == NULL) {        
    return NULL; 
    /* exception thrown */    
  }    
  (*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
  /* Construct a java.lang.String object */ 
  result = (*env)->NewObject(env, stringClass, cid, elemArr);
  /* Free local references */    
  (*env)->DeleteLocalRef(env, elemArr);    
  (*env)->DeleteLocalRef(env, stringClass);   
  return result; 
}

解释一下上面的例子, 首先FindClass返回java.lang.String的类引用。然后GetMethodID返回构造函数String(char[])的ID。NewCharArray创建缓冲区存储字符串。NewObject根据函数ID调用构造函数。NewObject函数需要的参数有:类的引用,构造方法的ID,构造方法需要的参数。DeleteLocalRed用来释放临时变量的资源,后面再做介绍。
由于String很常用,所以JNI内置了更高效的NewString来替代上述JNI调用构造函数的过程。
CallNonvirtualVoidMethod调用构造函数是可行的,不过本地函数首先需要调用AllocObject创建一个未初始化的对象(uninitialize object):

//替换result = (*env)->NewObject(env, stringClass, cid, elemArr);
result = (*env)->AllocObject(env, stringClass); 
if (result) {    
  (*env)->CallNonvirtualVoidMethod(env, result, stringClass, cid, elemArr);    
  /* we need to check for possible exceptions */    
  if ((*env)->ExceptionCheck(env)) {        
    (*env)->DeleteLocalRef(env, result);        
    result = NULL;    
  } 
}

使用AllocObject创建未初始化对象时一定要小心,注意不要对同一个对象调用多次构造函数。不过还是建议使用NewObject的方式,避免使用AllocObject/CallNonvirtualVoidMethod

缓存字段和方法ID

获取字段和方法ID,需要根据名字和描述符进行检索,而检索的过程是很耗费资源的。下面介绍一个如何使用缓存技术来减少消耗。缓存字段和方法ID主要有两种方法,区别在缓存的时刻: 在字段和方法ID被使用的时候,或者定义字段和方法的类静态初始化的时候。

在使用时缓存

在本地方法访问字段和方法的时候缓存它们的ID,下面的例子实现了将字段ID缓存在一个静态变量之中。

JNIEXPORT void JNICALL 
Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj) {
  static jfieldID fid_s = NULL; 
  /* cached field ID for s */
  jclass cls = (*env)->GetObjectClass(env, obj);    
  jstring jstr;    
  const char *str;
  if (fid_s == NULL) { 
    fid_s = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");if (fid_s == NULL) {            
      return; 
      /* exception already thrown */        
    }    
  }
  printf("In C:\n");
  jstr = (*env)->GetObjectField(env, obj, fid_s);    
  str = (*env)->GetStringUTFChars(env, jstr, NULL);    
  if (str == NULL) {        
    return; 
    /* out of memory */    
  }    
  printf("  c.s = \"%s\"\n", str);    
  (*env)->ReleaseStringUTFChars(env, jstr, str);
  jstr = (*env)->NewStringUTF(env, "123");    
  if (jstr == NULL) {        
    return; 
    /* out of memory */    
  }    
  (*env)->SetObjectField(env, obj, fid_s, jstr); 
}

fid_s缓存了INstanceFieldAccess.s的字段ID,初始化为NULL,第一次访问时被赋值。有人可能会说上述代码在多线程的时候会导致冲突,该缓存的值可能会被其他线程覆盖。但实际上影响不大,因为不同线程计算同一个字段的ID值是相等的。
同样的方法缓存方法ID:

jstring 
MyNewString(JNIEnv *env, jchar *chars, jint len) {    
  jclass stringClass;    
  jcharArray elemArr;    
  static jmethodID cid = NULL;    
  jstring result;
  stringClass = (*env)->FindClass(env, "java/lang/String");    
  if (stringClass == NULL) {        
    return NULL; 
    /* exception thrown */    
  }
  /* Note that cid is a static variable */    
  if (cid == NULL) {        
    /* Get the method ID for the String constructor */ 
    cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V");
    if (cid == NULL) {            
      return NULL; 
      /* exception thrown */        
    }    
  }
  /* Create a char[] that holds the string characters */    
  elemArr = (*env)->NewCharArray(env, len);    
  if (elemArr == NULL) {        
    return NULL; 
    /* exception thrown */    
  }    
  (*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
  /* Construct a java.lang.String object */ 
  result = (*env)->NewObject(env, stringClass, cid, elemArr);
  /* Free local references */    
  (*env)->DeleteLocalRef(env, elemArr);    
  (*env)->DeleteLocalRef(env, stringClass);    
  return result; 
}

类的静态初始化过程缓存

上一种方法中,每一次都需要检查ID是否已经缓存。在很多情况下,在使用前就已经初始化并缓存ID是很方便的。Java虚拟机在调用类方法之前都会执行类的静态初始化(static initializer)过程。因此可以在静态初始化过程缓存字段和方法的ID。看个例子:

class InstanceMethodCall {    
  private static native void initIDs();    
  private native void nativeMethod();    
  private void callback() {        
    System.out.println("In Java");    
  }    
  public static void main(String args[]) {        
    InstanceMethodCall c = new InstanceMethodCall();        c.nativeMethod();    
  }    
  static {        
    System.loadLibrary("InstanceMethodCall");        
    initIDs();    
  } 
}

本地代码实现:

#include <jni.h>
#include <stdio.h>
#include "InstanceMethodCall.h"

jmethodID MID_InstanceMethodCall_callback;
JNIEXPORT void JNICALL 
Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls) {    
    MID_InstanceMethodCall_callback = (*env)->GetMethodID(env, cls, "callback", "()V"); 
}

JNIEXPORT void JNICALL 
Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj) {  
    printf("In C\n");    
    (*env)->CallVoidMethod(env, obj, MID_InstanceMethodCall_callback); 
}

Java虚拟机在静态初始化InstanceMethodCall的时候执行了initIDs,缓存了方法ID,在INstanceMethodCall.nativeMethod中不再需要检索。

两种缓存方法的对比

如果开发者不能修改字段和方法所在类的源码,那么使用第一种方法(用时缓存)是合理的。比如,我们不能在java.lang.String中插入initIDs
用时缓存有几个缺点:

  • 每次都需要检查,而且多线程时可能还会重复检索。
  • ID在类unloaded之前才有效,要确保在本地方法还需要这些ID时,这个类不会unloaded或者reloaded(下一章会介绍JNI如何通过创建一个类引用来防止类unloaded)。而静态初始化会在类reloaded的时候重新计算字段和方法的ID。

综上,推荐在类的静态初始化时缓存字段和方法的ID。

JNI中操作Java类字段和方法的性能

学习完使用缓存来提高效率之后,你可能会想了解JNI访问字段和方法的效率,native/Java回调对比起Java/native调用和Java/Java调用效率怎样?
上述的疑问的取决与JVM执行JNI的效率。本文会通过分析调用本地方法回调和JNI操作字段和方法的过程来给出大概的概念。
对比一下Java/native调用和Java/Java调用:

  • Java/native调用相比于Java虚拟机内部的Java/Java调用来说多了一个调用转换的过程。当Java切换到本地方法时,Java虚拟机需要花费额外的操作来创建参数和栈帧。
  • 内联Java/native调用要比内联Java/Java调用要更困难。

估计执行Java/native调用要比Java/Java调用慢2-3倍,而Java/native调用和native/Java回调效率差不多。
JNI访问字段的花费主要取决于通过JNIEnv调用的成本。以释放一个对象来说,本地方法必须调用一个C函数来释放对象,这个C函数隔绝了本地代码和Java虚拟机的内部对象。但是JNI访问字段的花销可以忽略不计。


  • 第一部分,简介和JNI入门

JNI程序规范和指南1——简介
JNI程序规范和指南2——一个简单的例子

  • 第二部分,JNI指南

JNI程序规范和指南3——基本类型, 字符串和数组
JNI程序规范和指南4——字段和方法
JNI程序规范和指南5——JNI中的局部引用和全局引用
JNI程序规范和指南6——异常

  • 第三部分,JNI规范

建议不要用于商业用途, 转载请注明原文地址: https://Soo-Q6.github.io/blog/2019-09-30-JNI-guides-and-specifications-4/


© 2019. All rights reserved.

Powered by shouqin v1.0