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 类定义了对象的字段 s main 函数则创建了一个对象,为字段赋值,然后调用 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还支持 GetIntField SetFloatField

字段描述符(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的方法不同: GetStaticFieldID GetFieldID
  • 在访问字段的方法中( 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.f obj=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");