JNI的其中一种应用就是使用已经存在的原生库编写适合项目的原生函数。现代的操作系统都自带了很多的原生库,比如标准C库、POSIX库等,在编写项目的时候,有一部分的原生代码需要使用某些功能,而这些功能已经在系统的原生库中存在,那我们就可以直接使用这些已经存在的库中的功能了,比如项目的原生代码需要在终端输出信息(需要使用标准C库的printf系列函数)。
本章中将介绍一种典型的方法:创建一个类,在类中封装一系列的已经存在的原生库中的函数。比如,我们创建类C,在C中定义一个函数void f()映射到printf()函数,f()函数的功能就是把从Java传来的参数转换为printf()能够接受的参数。
本章首先讨论了编写封装类最直接的方式:一对一映射。然后再讨论共享桩,简化编写封装类的过程。在本章的最后会讨论如何使用peer classes封装已经存在的原生函数。
9.1 一对一映射
我们用简单的例子来说明。假如要编写一个封装类,这个类封装标准C库的atol函数:
long atol(const char str);
atol()函数分析传入的字符串(如字符串为”1234”),返回这个字符串代表的整数(long类型的1234)。封装类定义如下:
public class C {
public static native int atol(String str);
…
}
本章使用C++来实现原生方法。C.atol()的C++实现版本如下:
JNIEXPORT jint JNICALL Java_C_atol(JNIEnv env, jclass cls, jstring str)
{
const char cstr = env->GetStringUTFChars(str, 0);
if (cstr == NULL) {
return 0; / 内存耗尽 /
}
int result = atol(cstr);//调用系统的atop()函数完成核心功能
env->ReleaseStringUTFChars(str, cstr);
return result;
}
上述实现非常直接,使用GetStringUTFChars()函数获取字符串内容,然后使用C库的函数atol()获取字符串代表的整数,最后返回整数。
在这个例子中,
public static native int atol(String str);
这个函数的参数为String类型,被映射成为了系统函数atol()所需的const char 类型。
int result = atol(cstr); //cstr是const char 类型
String类型映射成了const char 类型。而Java平台的String类型是平台无关的类型,const char 类型却是与特定的操作系统有关的类型(如Linux与Windows中的char定义不相同)。在需要代码移植的时候,只需要移植JNIEXPORT jint JNICALL Java_C_atol()函数,而不需要移植使用C.atol()的应用代码。
下面再举一个更加复杂一点的例子。这次传递的参数是结构体指针。假如想要编写一个封装类封装Win32 API中的CreateFile()函数,CreateFile()函数原型如下:
typedef void
typedef long DWORD;
typedef struct {…} SECURITY_ATTRIBUTES;
HANDLE CreateFile(
const char fileName,
DWORD desiredAccess,
DWORD shareMode,
SECURITY_ATTRIBUTES attrs, // 安全属性
DWORD creationDistribution, // 如何创建
DWORD flagsAndAttributes, // 文件属性
HANDLE templateFile
);
CreateFile()函数支持很多Win32平台特有的属性,而平台无关的Java API不支持这些属性。比如,CreateFile()函数可以被用于指定特定的文件属性和访问属性,用于打开Win32的命名管道和处理串口通信等。
下面介绍如何把CreateFile()函数封装在Win32这个类中:
public class Win32 {
public static native int CreateFile(
String fileName, //String映射为const char
int desiredAccess, //int映射为DWORD
int shareMode, //int映射为DWORD
int[] secAttrs, // 安全属性 int[]映射为SECURITY_ATTRIBUTES
int creationDistribution, // 如何创建 int映射为DWORD
int flagsAndAttributes, // 文件属性 int映射为DWORD
int templateFile); //int映射为HANDLE
…
}
可以看到,const char fileName一对一映射为String fileName,DWORD一对一映射为int类型,SECURITY_ATTRIBUTES结构体类型映射为int[]类型,void类型也映射为int类型。之所以把结构体映射为数组,是因为结构体与Java平台的类在底层布局方面可能存在不同。
C++实现上面的被封装的原生函数:
JNIEXPORT jint JNICALL Java_Win32_CreateFile( JNIEnv env,
jclass cls,
jstring fileName,
jint desiredAccess,
jint shareMode,
jintArray secAttrs,
jint creationDistribution,
jint flagsAndAttributes,
jint templateFile)
{
jint result = 0;
jint cSecAttrs = NULL;
if (secAttrs) {
cSecAttrs = env->GetIntArrayElements(secAttrs, 0);
if (cSecAttrs == NULL) {
return 0; / 内存耗尽 /
}
}
//使用工具函数获取文件名,而不是使用JNI的GetStringUTFChars()
char cFileName = JNU_GetStringNativeChars(env, fileName);
if (cFileName) {
/ 调用真正的Win32 API CreateFile()函数 /
result = (jint)CreateFile(cFileName,
desiredAccess,
shareMode,
(SECURITY_ATTRIBUTES )cSecAttrs,
creationDistribution,
flagsAndAttributes,
(HANDLE)templateFile);
free(cFileName);
}
/ 抛出异常 /
if (secAttrs) {
env->ReleaseIntArrayElements(secAttrs, cSecAttrs, 0);
}
return result;
}
上述程序中的工具函数JNU_GetStringNativeChars()在第八章的国际化编码部分编写的,用于把Unicode编码的字符串转换为本地编码的字符串。其他的内容无需多说,无非就是把Java传入的参数一对一的映射成JNI支持的类型,然后再调用真正的函数处理核心问题。
C.atol()和Win32.CreateFile()两个例子演示了编写封装类封装已经存在的原生函数(注意,这里的原生函数指的是已经存在的别人写的或系统自带的,不要与项目相关的我们自己编写的原生函数相混淆。在提到后一种时,我会说“与项目相关的原生函数”)的通常做法。每一个原生函数(如Win32系统的API CreateFile())映射为单一的”桩函数”(Java_Win32_CreateFile()),这个”桩函数”又映射到单一的项目相关的原生函数定义(Win32.CreateFile())。在这种一对一映射中,”桩函数”有两方面的作用:
1.桩函数向原生函数(注意,是已经存在的原生函数)传递参数时,必须符合Java虚拟机的命名规范,而且Java虚拟机还会向桩函数传递两个额外的参数JNIEnv指针和this指针,桩函数可以使用这两个参数做一些处理。
2.桩函数在Java类型与原生函数类型之间做转换,如jstring转换为const char 。
# 9.2 共享桩
一对一映射是单个桩函数在Java和原生函数之间起到桥接的作用,对每一个想要封装起来的原生函数要编写单独的单个桩函数。如果想要调用大量的系统调用的话,就必须要为每一个系统调用编写一对一映射的桩函数,这个工作量很大且没有效率。这一节介绍一个提高效率的封装原生函数的技术,称为共享桩。
共享桩就是一个我们自己编写的原生函数,它主要负责接受调用者的参数,对参数的类型做必要的转换,用转换后的参数调用其他的原生函数,对其返回值类型再做必要的转换,将转换后的返回值返回给其调用者。
首先,我们演示一下使用共享桩如何简化上一节的C.atol()的编写。
public class C {
private static CFunction c_atol =
new CFunction(“msvcrt.dll”, // 原生库的名称
“atol”, // 库中C函数的名称
“C”); // 符合标准C的调用规范
public static int atol(String str) {
return c_atol.callInt(new Object[] {str});
}
…
}
上一节的C.atol()是我们编写的单个原生函数完成一对一映射。这一节的共享桩中,C.atol()定义为CFunction类,在这个类的内部实现了共享桩。在类C中的静态变量C.c_atol指向一个CFunction对象,这个CFunction对象则指向msvcrt.dll库(Win32中的多线程库)中的C函数atol()。CFunction对象的构造函数调用表明atol()函数的调用符合标准C的调用规范。一旦c_atol变量被初始化,调用C.atol()函数会调用c_atol.callInt()函数,c_atol.callInt()函数就是所谓的共享桩函数。
在后面的小节中要实现CFunction类的继承层次如下:

从上面的继承层次中可以看出,CPointer类是Java平台的java.lang.Object的子类,CPointer类的对象用于指向任意的C指针。CFunction类是CPointer类的子类,CFunction类的对象更细化一些,指向任意的C函数指针:
public class CFunction extends CPointer {
public CFunction(String lib, // 原生库名称
String fname, // 库中C函数的名称
String conv) { // 要遵守的函数调用规范
…
}
public native int callInt(Object[] args);
…
}
callInt()函数的参数是包含java.lang.Object对象的数组,它检查数组元素的类型,根据需要做必要的转换(如jstring转换为const char ),再用转换后的参数调用底层的库函数。callInt()函数把底层库函数的返回值当做int类型返回。CFunction类也可以定义callFloat()、callDouble()等函数用于返回其他类型。
CPointer类定义如下:
public abstract class CPointer {
public native void copyIn(
int bOff, //C指针的偏移量
int[] buf, //原始数据
int off, //原始数据的偏移量
int len); //要复制的元素个数
public native void copyOut(…);
…
}
CPointer类是一个支持访问任意C指针的抽象类。如copyIn()方法就会从一个int类型数组中复制指定的元素到本地C指针指向的位置。在使用这个方法的时候要额外注意,本地C指针指向的内存区域必须足够大能够放下复制过来的元素,否则就会导致内存容量不够而覆盖了后面内存中的内容。使用CPointer.copyIn()函数同样像直接使用C指针那样不安全,需要额外的给予关注。
CMalloc类是CPointer类的子类,它用于表示使用malloc()函数在堆上分配的一块内存区域,其定义如下:
public class CMalloc extends CPointer {
public CMalloc(int size) throws OutOfMemoryError { … }
public native void free();
…
}
CMalloc类的构造函数在堆上分配指定大小的内存区域。CMalloc.free()函数则用于释放这块内存区域。
下面,我们使用CFunction类和CMalloc类重新实现Win32.CreateFile()函数:
public class Win32 {
private static CFunction c_CreateFile =
new CFunction (“kernel32.dll”, //原生库名称
“CreateFileA”, //原生库中函数名称
“JNI”); //函数调用符合JNI规范
public static int CreateFile(
String fileName, //要创建的文件名称
int desiredAccess, //访问权限(读写)
int shareMode, //共享权限
int[] secAttrs, // 安全属性
int creationDistribution, // 如何创建这个文件
int flagsAndAttributes, // 文件属性
int templateFile)
{
CMalloc cSecAttr = null;
if(cSecAttr != null) {
cSecAttrs = new CMalloc(secAttrs.length 4);
cSecAttrs.copyIn(0, secAttrs, 0, secAttrs.length);
}
try {
return c_CreateFile.callInt(new Object[] {
fileName,
new Integer(desiredAccess),
new Integer(shareMode),
cSecAttrs,
new Integer(creationDistribution),
new Integer(flagsAndAttributes),
new Integer(templateFile)});
} finally {
if (secAttrs != null) {
cSecAttrs.free();
}
}
…
}
同样,将CFunction类的对象缓存在静态变量中,供后面使用。kernle32.dll库中Win32 API CreateFile()函数被封装为CreateFileA()函数。函数调用符合JNI调用规范,也就是符合Win32的标准调用规范(stdcall)。
Win32.CreateFile()函数首先在C的堆上分配了足够装下安全参数的临时的内存区域,然后把所有的参数放入数组中,并通过共享桩函数c_CreateFile.callInt()调用底层的CreateFileA()函数,最后将临时分配的内存区域释放掉。记住,我们在finally语句块中也要释放分配的内存空间,这样保证当callInt()函数抛出异常时也能不产生内存泄露。
# 9.3 一对一映射与共享桩的比较
一对一映射和共享桩是两种封装原生库的方法,它们有各自的优缺点。
共享桩最大的优势就是程序员不用在项目中为每一个要封装的原生函数编写对应的桩函数(当要封装的原生函数很多时,对应的桩函数也很多,工作量大且效率低)。当然,使用共享桩的时候也要注意,这是在Java平台上编写C代码,因此Java平台不能保证类型安全。如果使用共享桩函数时疏忽犯了错误,则可能导致程序崩溃或内存溢出。
使用一对一映射的好处是在Java类型与原生类型之间进行转换是非常高效的。而共享桩则是能够处理一系列预先定义好的参数类型,但对这些参数不能实现性能优化。比如调用共享桩CFunction.callInt()方法,总是需要为int类型的参数创建一个对应的Integer对象,增加了额外的空间和时间消耗。
在实际项目中,要衡量性能、可移植性和短期生产效率等更方面因素。共享桩最好用于使用现成的不用移植的原生代码中,此时对性能要求不能太高。而一对一映射则可以用在要求可移植和高性能的情况。
# 9.4 实现共享桩
到目前为止,我们仅仅给出了CFunction、CPointer和CMalloc类的部分定义,对于其内部的方法如何实现的则没有提及。本小节讨论如何使用基本的JNI特性实现这些方法和类。
## 9.4.1 CPointer类
CPointer类是CFunction和CMalloc类的父类,首先实现它。抽象类CPointer拥有一个64比特的成员变量peer,用于指向底层的C指针:
public abstract class CPointer {
protected long peer;
public native void copyIn(int bOff, int[] buf,int off,int len);
public native void copyOut(…);
…
}
其中copyIn()函数使用C++实现如下:
JNIEXPORT void JNICALL Java_CPointer_copyIn__I_3III(JNIEnv env, jobject self, jint boff, jintArray arr, jint off, jint len)
{
//获取类CPointer的成员变量peer
long peer = env->GetLongField(self, FID_CPointer_peer);
//调用JNI函数复制数组内容到peer指向的位置
env->GetIntArrayRegion(arr, off, len, (jint )peer + boff);
}
上述实现非常直接,其中FID_CPointer_peer是提前计算并缓存起来的CPointer.peer的ID。函数名称使用了长名称编码机制,该机制用于避免使用不同类型的参数对copyIn()的重载导致的命名冲突。
## 9.4.2 CMalloc类
CMalloc类使用两个原生方法来分配和释放内存:
public class CMalloc extends CPointer {
private static native long malloc(int size);
public CMalloc(int size) throws OutOfMemoryError {
peer = malloc(size);
if (peer == 0) {
throw new OutOfMemoryError();
}
}
public native void free();
…
}
CMalloc类的构造函数调用CMalloc.malloc()函数在C堆上创建给定大小的内存区域,如果分配失败,则抛出OutOfMemoryError异常。CMalloc.malloc()和CMalloc.free()函数实现如下:
JNIEXPORT jlong JNICALL Java_CMalloc_malloc(JNIEnv env, jclass cls, jint size)
{
return (jlong)malloc(size);
}
JNIEXPORT void JNICALL Java_CMalloc_free(JNIEnv env, jobject self)
{
long peer = env->GetLongField(self, FID_CPointer_peer);
free((void )peer);
}
9.4.3 CFunction类
实现CFunction类需要操作系统支持运行时动态链接的功能,也需要CPU相关的汇编代码。下面实现的版本是与Win32平台相关的,一旦你懂得了核心思想,就可以自己移植到其他平台。
CFunction类定义如下:
public class CFunction extends CPointer {
private static final int CONV_C = 0;//函数调用符合C调用规范
private static final int CONV_JNI = 1;//函数调用符合JNI调用规范
private int conv;
private native long find(String lib, String fname);//在原生库中查找函数
public CFunction(String lib,
String fname,
String conv) {
if (conv.equals(“C”)) {//使用C调用规范
conv = CONV_C;
} else if (conv.equals(“JNI”)) {//使用JNI调用规范
conv = CONV_JNI;
} else {//抛出异常
throw new IllegalArgumentException(“bad calling convention”);
}
peer = find(lib, fname);//按照调用规范调用find函数,peer是从CPointer继承来的
}
public native int callInt(Object[] args);//共享桩函数
…
}
CFunction.find()函数的实现如下:
JNIEXPORT jlong JNICALL Java_CFunction_find(JNIEnv env, jobject self, jstring lib, jstring fun)
{
void handle;
void func;
char libname;
char funname;
if ((libname = JNU_GetStringNativeChars(env, lib))) {//获取原生库名称
if ((funname = JNU_GetStringNativeChars(env, fun))) {//获取函数名
if ((handle = LoadLibrary(libname))) {//动态加载链接原生库
//如果从库中查找原生函数失败,则抛出异常
if (!(func = GetProcAddress(handle, funname))) {
JNU_ThrowByName(env, “java/lang/UnsatisfiedLinkError”, funname);
}
} else {//加载链接原生库出错,抛出异常
JNU_ThrowByName(env, “java/lang/UnsatisfiedLinkError”, libname);
}
free(funname);//释放不再使用的资源
}
free(lib name);//释放不再使用的资源
}
return (jlong)func;//返回找到的函数的指针(在原生库中的地址)。
}
CFunction.callInt()函数负责调用真正的底层原生函数,其实现如下:
JNIEXPORT jint JNICALL Java_CFunction_callInt(JNIEnv env, jobject self, jobjectArray arr)//第三个参数是调用底层原生函数所需的参数,可能需要进行类型转换
{
#define MAX_NARGS 32
jint ires;
int nargs, nwords;
jboolean is_string[MAX_NARGS];//记录在参数列表args中,哪些是String类型
word_t args[MAX_NARGS];//转换后的参数列表
nargs = env->GetArrayLength(arr);//确定参数的个数
if (nargs > MAX_NARGS) {//参数太多
JNU_ThrowByName(env, “java/lang/IllegalArgumentException”, “too many arguments”);
return 0;
}
// 下面的循环是在转换参数类型
for (nwords = 0; nwords < nargs; nwords++) {
is_string[nwords] = JNI_FALSE;
jobject arg = env->GetObjectArrayElement(arr, nwords);//获取第nwords参数
if (arg == NULL) {//没有参数
args[nwords].p = NULL;
} else if (env->IsInstanceOf(arg, Class_Integer)) {//参数是Integer类对象
//获取Integer对象的整型值,FID_Integer_value是提前计算好的变量ID
args[nwords].i = env->GetIntField(arg, FID_Integer_value);
} else if (env->IsInstanceOf(arg, Class_Float)) {//参数是Float对象
//获取Float对象的浮点值,同样FID_Float_value也是提前计算好的变量ID
args[nwords].f = env->GetFloatField(arg, FID_Float_value);
} else if (env->IsInstanceOf(arg, Class_CPointer)) {//参数是C指针
//把C指针转换为long类型,获取其值
args[nwords].p = (void ) env->GetLongField(arg, FID_CPointer_peer);
} else if (env->IsInstanceOf(arg, Class_String)) {//参数是String类型
//把String类型转换为本地编码字符串
char cstr = JNU_GetStringNativeChars(env, (jstring)arg);
if ((args[nwords].p = cstr) == NULL) {
goto cleanup; // 抛出异常
}
is_string[nwords] = JNI_TRUE;//第nwords是String类型
} else {//位置类型,抛出异常
JNU_ThrowByName(env, “java/lang/IllegalArgumentException”, “unrecognized argument type”);
goto cleanup;
}
env->DeleteLocalRef(arg);//及时删除不再使用的局部引用
}//arr中所有的参数类型均转换完毕,转换后存放在args数组中
//FID_CPointer_peer是CPointer.peer的ID,已经提前计算并缓存
void func = (void )env->GetLongField(self, FID_CPointer_peer);
int conv = env->GetIntField(self, FID_CFunction_conv);//得到调用规范
// 现在调用func函数,并获取其返回值
ires = asm_dispatch(func, nwords, args, conv);
cleanup:
// 释放上面创建的所有字符串
for (int i = 0; i < nwords; i++) {
if (is_string[i]) {
free(args[i].p);
}
}
return ires;
}
上述实现中,我们假定提前计算并缓存了成员变量的ID,比如FID_CPointer_peer是CPointer.peer的ID,FID_CFunction_conv是CFunction.conv的ID,Class_String是java.lang.String类对象的全局引用等等。word_t类型是一个联合体,用于表示一个机器字,定义如下:
typedef union {
jint i;
jfloat f;
void p;
} word_t;
int asm_dispatch(void func, // 底层原生函数指针
int nwords, // 参数数组中的参数个数
word_t *args, // 参数数组地址
int conv) //调用规范,0-标准C规范,1-JNI规范
__asm {
mov esi, args
mov edx, nwords
// word address -> byte address
shl edx, 2
sub edx, 4
jc args_done
// 首先将最后一个参数压入调用栈(函数从右至左压入参数)
args_loop:
mov eax, DWORD PTR [esi+edx]
push eax
sub edx, 4
jge SHORT args_loop
args_done:
call func
// 检查调用规范
mov edx, conv
or edx, edx
jnz jni_call
// 弹出参数
mov edx, nwords
shl edx, 2
add esp, edx
jni_call:
// done, return value in eax
}
}
上面的汇编代码首先把传入的参数数组中的元素复制到底层原生函数func()的栈中,然后调用func(),等func()返回后,再检查func()的调用规范。如果func()遵守标准C调用规范,asm_dispatch()函数把参数从func()的栈中弹出;如果func()遵守JNI调用规范,则不用弹出参数,func()函数会在返回之前自动把参数弹出栈。
9.5 Peer类
一对一映射和共享桩都是用来处理封装底层原生函数的问题的。而我们在创建共享桩的时候也遇到了封装原生数据结构的问题。再来看一下CPointer类的定义:
public abstract class CPointer {
protected long peer;
public native void copyIn(int bOff, int[] buf,int off,int len);
public native void copyOut(...);
...
}
CPointer类有一个64比特的成员变量peer,用于指向原生数据结构(在本章的例子中,peer指向一片内存区域)。peer变量的具体含义由CPointer类的子类决定。比如,CMalloc类中的peer变量指向C堆中的内存区域。
直接与原生数据结构相关的类称为Peer类,如CPointer、CMalloc。可以为大量的原生数据结构创建Peer类,包括:
1.文件描述符
2.套接字描述符
3.windows或其他图形界面的组件
## 9.5.1 Java平台的Peer类
当前的JDK和Java 2 SDK(1.1或1.2)在内部使用Peer类实现java.io,java.net和java.awt等包。一个java.io.FileDescriptor类对象有一个指向本地文件描述符的私有成员变量fd:
// java.io.FileDescriptor类定义
public final class FileDescriptor {
private int fd;
...
}
public class CMalloc extends CPointer {
public native synchronized void free();
protected void finalize() {
free();
}
...
}
JNIEXPORT void JNICALL Java_CMalloc_free(JNIEnv *env, jobject self)
{
long peer = env->GetLongField(self, FID_CPointer_peer);
if (peer == 0) {
return; /* peer在之前已经被释放了,直接返回 */
}
free((void *)peer);
peer = 0;
env->SetLongField(self, FID_CPointer_peer, peer);
}
peer = 0;
env->SetLongField(self, FID_CPointer_peer, peer);
而不是一条语句:
env->SetLongField(self, FID_CPointer_peer, 0);
这是因为,某些C++编译器会把常量0作为32比特的整型,而不是64比特。当然,C++可以指定64比特的常量,但是这样移植性差。
在Peer类中使用finalize方法是保险的做法,但是你不应该将其作为释放本地数据结构的唯一方法。因为本地数据结构很可能比它的Peer类占用更多的内存资源,Java虚拟机在回收Peer类对象时同时也要释放大量的内存资源,速度会变慢。
如果手动释放本地数据结构的话,必须要保证在每一条返回路径之前都要调用free()方法,否则可能会造成内存泄露。特别注意在Peer类执行期间会抛出异常,在异常处理中也要记得调用free()方法。
9.5.3 Peer类的回调指针
上面的几节提到过,Peer类中拥有指向本地数据结构的私有变量。但有时候,在本地数据结构中也包含指向其对应的Peer类对象的引用。比如,在原生代码中需要初始化Peer类对象的成员方法。
比如,我们创建一个虚拟的用户界面组件KeyInput,它的原生C++组件称为key_input。当用户按下按键之后,操作系统就会回调key_input组件的key_pressed()函数作为对这一事件的响应,而key_input组件通过在key_pressed()函数中回调KeyInput组件的KeyPressed()方法,向KeyInput组件传达这个事件的响应。下图的箭头指示了用户按键的响应是如何从操作系统传达到KeyInput组件的。
Peer类KeyInput定义如下:
class KeyInput {
private long peer;
private native long create();
private native void destroy(long peer);
public KeyInput() {
peer = create();
}
public destroy() {
destroy(peer);
}
private void keyPressed(int key) {
… / 处理按键事件 /
}
}
上述定义中,原生函数create()用于为C++结构体key_input的对象分配内存空间。下面是结构体key_input、create()和destroy()函数的定义:
struct key_input {
jobject back_ptr; // Peer类对象的回调指针
int key_pressed(int key); // 被操作系统调用
};
JNIEXPORT jlong JNICALL Java_KeyInput_create(JNIEnv env, jobject self)
{
key_input cpp_obj = new key_input();
cpp_obj->back_ptr = env->NewGlobalRef(self);
return (jlong)cpp_obj;
}
JNIEXPORT void JNICALL Java_KeyInput_destroy(JNIEnv env, jobject self, jlong peer)
{
key_input cpp_obj = (key_input*)peer;
env->DeleteGlobalRef(cpp_obj->back_ptr);
delete cpp_obj;
return;
}
原生函数create()用于为结构体分配内存空间,并用KeyInput类对象的全局引用初始化其回调指针back_ptr。free()函数则相反,释放掉back_ptr和C++结构体。KeyInput类的构造函数会调用create()函数在它和它对应的原生部分建立联系:
当用户按下按键,操作系统回调C++成员方法key_input::key_pressed(),这个成员方法通过回调KeyInput::KeyPressed()方法来响应这个事件。key_input::key_pressed()方法定义如下:
// 返回0表示成功,返回-1表示失败
int key_input::key_pressed(int key)
{
jboolean has_exception;
JNIEnv *env = JNU_GetEnv();//工具函数,在第8章实现过
//下面的工具函数在第6章实现过,根据方法名字调用方法
JNU_CallMethodByName(env,
&has_exception,
java_peer,
“keyPressed”,
“()V”,
key);
if (has_exception) {//如果发生了异常,就清除挂起的异常,立即返回
env->ExceptionClear();
return -1;
} else { //否则,返回成功
return 0;
}
}
在结束本小节之前,再讨论一个小问题。假如,为了避免潜在的内存泄露问题,你再KeyInput类中假如了finalize方法:
class KeyInput {
…
public synchronized destroy() {
if (peer != 0) {
destroy(peer);
peer = 0;
}
}
protect void finalize() {
destroy();
}
}
其中,destroy()方法被定义为同步方法,避免多线程竞争。在destroy()方法内首先检查peer变量是否为0,然后调用其重载的destroy()方法释放peer变量指向的资源,并将其值赋为0.
然而,上述代码并不会达到预期目的。除非你显式的调用destroy()方法,否则虚拟机永远都不会回收任何的KeyInput类对象。
在前面的实现代码中可以看到,KeyInput类构造函数会调用原生函数KeyInput::create(),而create()函数创建了KeyInput对象的JNI全局引用,用于初始化C++结构体key_input的回调指针。这样一来,这个全局引用就会阻止虚拟机回收任何的KeyInput对象。解决的办法就是用弱全局引用替代全局引用:
JNIEXPORT jlong JNICALL Java_KeyInput_create(JNIEnv env, jobject self)
{
key_input cpp_obj = new key_input();
cpp_obj->back_ptr = env->NewWeakGlobalRef(self);
return (jlong)cpp_obj;
}
JNIEXPORT void JNICALL Java_KeyInput_destroy(JNIEnv env, jobject self, jlong peer)
{
key_input cpp_obj = (key_input*)peer;
env->DeleteWeakGlobalRef(cpp_obj->back_ptr);
delete cpp_obj;
return;
}
好了,本章的内容到此结束。在本章中,介绍了封装已经存在的原生库中的原生函数的两种方法:一对一映射和使用共享桩;也介绍了封装本地数据结构的Peer类。