JNI程序规范和指南4——字段和方法
in 个人博客 on 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");