转的一篇文章,看的不懂,先坐下记录吧~~~java
java跨平台的特性使Java愈来愈受开发人员的欢迎,但也每每会听到很多的抱怨:用Java开发的图形用户窗口界面每次在启动的时候都会跳出一个控制台窗口,这个控制台窗口让原本很是棒的界面失色很多。怎么可以让经过Java开发的GUI程序不弹出Java的控制台窗口呢?其实如今不少流行的开发环境例如JBuilder、Eclipse都是使用纯Java开发的集成环境。这些集成环境启动的时候并不会打开一个命令窗口,由于它使用了JNI(Java Native Interface)的技术。经过这种技术,开发人员不必定要用命令行来启动Java程序,能够经过编写一个本地GUI程序直接启动Java程序,这样就可避免另外打开一个命令窗口,让开发的Java程序更加专业。ios
JNI答应运行在虚拟机的Java程序可以与其它语言(例如C和C++)编写的程序或者类库进行相互间的调用。同时JNI提供的一整套的API,答应将Java虚拟机直接嵌入到本地的应用程序中。图1是Sun站点上对JNI的基本结构的描述。数组
本文将介绍如何在C/C++中调用Java方法,并结合可能涉及到的问题介绍整个开发的步骤及可能碰到的难题和解决方法。本文所采用的工具是Sun公司建立的 Java Development Kit (JDK) 版本 1.3.1,以及微软公司的Visual C++ 6开发环境。多线程
环境搭建app
为了让本文如下部分的代码可以正常工做,咱们必须创建一个完整的开发环境。首先须要下载并安装JDK 1.3.1,其下载地址为“http://java.sun.com”。假设安装路径为C:\JDK。下一步就是设置集成开发环境,经过Visual C++ 6的菜单Tools→Options打开选项对话框如图2。框架
将目录C:\JDK\include和C:\JDK\include\win32加入到开发环境的Include Files目录中,同时将C:\JDK\lib目录添加到开发环境的Library Files目录中。这三个目录是JNI定义的一些常量、结构及方法的头文件和库文件。集成开发环境已经设置完毕,同时为了执行程序须要把Java虚拟机所用到的动态连接库所在的目录C:\JDK \jre\bin\classic设置到系统的Path环境变量中。这里须要提出的是,某些开发人员为了方便直接将JRE所用到的DLL文件直接拷贝到系统目录下。这样作是不行的,将致使初始化Java虚拟机环境失败(返回值-1),缘由是Java虚拟机是以相对路径来寻找所用到的库文件和其它一些相关文件的。至此整个JNI的开发环境设置完毕,为了让这次JNI旅程可以顺利进行,还必须先预备一个Java类。在这个类中将用到Java中几乎全部有表明性的属性及方法,如静态方法与属性、数组、异常抛出与捕捉等。咱们定义的Java程序(Demo.java)以下,本文中全部的代码演示都将基于该Java程序,代码以下:jvm
package jni.test; /** * 该类是为了演示JNI如何访问各类对象属性等 * @author liudong */ public class Demo { //用于演示如何访问静态的基本类型属性 public static int COUNT = 8; //演示对象型属性 public String msg; PRivate int[] counts; public Demo() { this("缺省构造函数"); } /** * 演示如何访问构造器 */ public Demo(String msg) { System.out.println(":" + msg); this.msg = msg; this.counts = null; } /** * 该方法演示如何访问一个访问以及中文字符的处理 */ public String getMessage() { return msg; } /** * 演示数组对象的访问 */ public int[] getCounts() { return counts; } /** * 演示如何构造一个数组对象 */ public void setCounts(int[] counts) { this.counts = counts; } /** * 演示异常的捕捉 */ public void throwExcp() throws IllegalaccessException { throw new IllegalAccessException("exception occur."); } }ide
初始化虚拟机函数
本地代码在调用Java方法以前必须先加载Java虚拟机,然后全部的Java程序都在虚拟机中执行。为了初始化Java虚拟机,JNI提供了一系列的接口函数Invocation API。经过这些API能够很方便地将虚拟机加载到内存中。建立虚拟机能够用函数 jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args)。对于这个函数有一点须要注重的是,在JDK 1.1中第三个参数老是指向一个结构JDK1_ 1InitArgs, 这个结构没法彻底在全部版本的虚拟机中进行无缝移植。在JDK 1.2中已经使用了一个标准的初始化结构JavaVMInitArgs来替代JDK1_1InitArgs。下面咱们分别给出两种不一样版本的示例代码。工具
在JDK 1.1初始化虚拟机:
#include int main() { JNIEnv *env; JavaVM *jvm; JDK1_1InitArgs vm_args; jint res; /* IMPORTANT: 版本号设置必定不能漏 */ vm_args.version = 0x00010001; /*获取缺省的虚拟机初始化参数*/ JNI_GetDefaultJavaVMInitArgs(&vm_args); /* 添加自定义的类路径 */ sprintf(classpath, "%s%c%s", vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH); vm_args.classpath = classpath; /*设置一些其余的初始化参数*/ /* 建立虚拟机 */ res = JNI_CreateJavaVM(&jvm,&env,&vm_args); if (res < 0) { fprintf(stderr, "Can't create Java VM\n"); exit(1); } /*释放虚拟机资源*/ (*jvm)->DestroyJavaVM(jvm); }
JDK 1.2初始化虚拟机:
/* invoke2.c */ #include int main() { int res; JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption options[3]; vm_args.version=JNI_VERSION_1_2;//这个字段必须设置为该值 /*设置初始化参数*/ options[0].optionString = "-Djava.compiler=NONE"; options[1].optionString = "-Djava.class.path=."; options[2].optionString = "-verbose:jni";//用于跟踪运行时的信息 /*版本号设置不能漏*/ vm_args.version = JNI_VERSION_1_2; vm_args.nOptions = 3; vm_args.options = options; vm_args.ignoreUnrecognized = JNI_TRUE; res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); if (res < 0) { fprintf(stderr, "Can't create Java VM\n"); exit(1); } (*jvm)->DestroyJavaVM(jvm); fprintf(stdout, "Java VM destory.\n"); }
为了保证JNI代码的可移植性,建议使用JDK 1.2的方法来建立虚拟机。JNI_CreateJavaVM函数的第二个参数JNIEnv *env,就是贯穿整个JNI始末的一个参数,由于几乎全部的函数都要求一个参数就是JNIEnv *env。
访问类方法
初始化了Java虚拟机后,就能够开始调用Java的方法。要调用一个Java对象的方法必须通过几个步骤:
1.获取指定对象的类定义(jclass)
有两种途径来获取对象的类定义:第一种是在已知类名的状况下使用FindClass来查找对应的类。可是要注重类名并不一样于平时写的Java代码,例如要获得类jni.test.Demo的定义必须调用以下代码:
jclass cls = (*env)->FindClass(env, "jni/test/Demo");//把点号换成斜杠
而后经过对象直接获得其所对应的类定义:
jclass cls = (*env)-> GetObjectClass(env, obj); //其中obj是要引用的对象,类型是jobject
2.读取要调用方法的定义(jmethodID)
咱们先来看看JNI中获取方法定义的函数:
jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass class, const char *name, const char *sig);
这两个函数的区别在于GetStaticMethodID是用来获取静态方法的定义,GetMethodID则是获取非静态的方法定义。这两个函数都须要提供四个参数:env就是初始化虚拟机获得的JNI环境;第二个参数class是对象的类定义,也就是第一步获得的obj;第三个参数是方法名称;最重要的是第四个参数,这个参数是方法的定义。由于咱们知道Java中答应方法的多态,仅仅是经过方法名并无办法定位到一个具体的方法,所以须要第四个参数来指定方法的具体定义。可是怎么利用一个字符串来表示方法的具体定义呢?JDK中已经预备好一个反编译工具javap,经过这个工具就能够获得类中每一个属性、方法的定义。下面就来看看jni.test.Demo的定义:
打开命令行窗口并运行 javap -s -p jni.test.Demo 获得运行结果以下:
Compiled from Demo.java public class jni.test.Demo extends java.lang.Object { public static int COUNT; /* I */ public java.lang.String msg; /* Ljava/lang/String; */ private int counts[]; /* [I */ public jni.test.Demo(); /* ()V */ public jni.test.Demo(java.lang.String); /* (Ljava/lang/String;)V */ public java.lang.String getMessage(); /* ()Ljava/lang/String; */ public int getCounts()[]; /* ()[I */ public void setCounts(int[]); /* ([I)V */ public void throwExcp() throws java.lang.IllegalAccessException; /* ()V */ static {}; /* ()V */ }
咱们看到类中每一个属性和方法下面都有一段注释。注释中不包含空格的内容就是第四个参数要填的内容(关于javap具体参数请查询JDK的使用帮助)。下面这段代码演示如何访问jni.test.Demo的getMessage方法:
/* 假设咱们已经有一个jni.test.Demo的实例obj */ jmethodID mid; jclass cls = (*env)-> GetObjectClass (env, obj);//获取实例的类定义 mid=(*env)->GetMethodID(env,cls,"getMessage"," ()Ljava/lang/String; "); /*假如mid为0表示获取方法定义失败*/ jstring msg = (*env)-> CallObjectMethod(env, obj, mid); /* 假如该方法是静态的方法那只须要将最后一句代码改成如下写法便可: jstring msg = (*env)-> CallStaticObjectMethod(env, cls, mid); */
3.调用方法
为了调用对象的某个方法,可使用函数CallMethod或者CallStaticMethod(访问类的静态方法),根据不一样的返回类型而定。这些方法都是使用可变参数的定义,假如访问某个方法须要参数时,只须要把全部参数按照顺序填写到方法中就能够。在讲到构造函数的访问时,将演示如何访问带参数的构造函数。
访问类属性
访问类的属性与访问类的方法大致上是一致的,只不过是把方法变成属性而已。
1.获取指定对象的类(jclass)
这一步与访问类方法的第一步彻底相同,具体使用参看访问类方法的第一步。
2.读取类属性的定义(jfieldID)
在JNI中是这样定义获取类属性的方法的:
jfieldID (JNICALL *GetFieldID) (JNIEnv *env, jclass clazz, const char *name, const char *sig); jfieldID (JNICALL *GetStaticFieldID) (JNIEnv *env, jclass clazz, const char *name, const char *sig);
这两个函数中第一个参数为JNI环境;clazz为类的定义;name为属性名称;第四个参数一样是为了表达属性的类型。前面咱们使用javap工具获取类的具体定义的时候有这样两行:
public java.lang.String msg; /* Ljava/lang/String; */
其中第二行注释的内容就是第四个参数要填的信息,这跟访问类方法时是相同的。
3.读取和设置属性值
有了属性的定义要访问属性值就很轻易了。有几个方法用来读取和设置类的属性,它们是:GetField、SetField、GetStaticField、SetStaticField。好比读取Demo类的msg属性就能够用GetObjectField,而访问COUNT用GetStaticIntField,相关代码以下:
jfieldID field = (*env)->GetFieldID(env,obj,"msg"," Ljava/lang/String;"); jstring msg = (*env)-> GetObjectField(env, cls, field);//msg就是对应Demo的msg jfieldID field2 = (*env)->GetStaticFieldID(env,obj,"COUNT","I"); jint count = (*env)->GetStaticIntField(env,cls,field2);
访问构造函数
不少人刚刚接触JNI的时候每每会在这一节碰到问题,查遍了整个jni.h看到这样一个函数NewObject,它应该是能够用来访问类的构造函数。可是该函数须要提供构造函数的方法定义,其类型是jmethodID。从前面的内容咱们知道要获取方法的定义首先要知道方法的名称,可是构造函数的名称怎么来填写呢?其实访问构造函数与访问一个普通的类方法大致上是同样的,唯一不一样的只是方法名称不一样及方法调用时不一样而已。访问类的构造函数时方法名必须填写“”。下面的代码演示如何构造一个Demo类的实例:
jclass cls = (*env)->FindClass(env, "jni/test/Demo"); /** 首先经过类的名称获取类的定义,至关于Java中的Class.forName方法 */ if (cls == 0) jmethodID mid = (*env)->GetMethodID(env,cls,"","(Ljava/lang/String;)V "); if(mid == 0) jobject demo = jenv->NewObject(cls,mid,0); /** 访问构造函数必须使用NewObject的函数来调用前面获取的构造函数的定义 上面的代码咱们构造了一个Demo的实例并传一个空串null */
数组处理
建立一个新数组
要建立一个数组,咱们首先应该知道数组元素的类型及数组长度。JNI定义了一批数组的类型jArray及数组操做的函数NewArray,其中就是数组中元素的类型。例如,要建立一个大小为10而且每一个位置值分别为1-10的整数数组,编写代码以下:
int i = 1; jintArray array;//定义数组对象 (*env)-> NewIntArray(env, 10); for(; i<= 10; i++) (*env)->SetIntArrayRegion(env, array, i-1, 1, &i);
访问数组中的数据
访问数组首先应该知道数组的长度及元素的类型。如今咱们把建立的数组中的每一个元素值打印出来,代码以下:
int i; /* 获取数组对象的元素个数 */ int len = (*env)->GetArrayLength(env, array); /* 获取数组中的全部元素 */ jint* elems = (*env)-> GetIntArrayElements(env, array, 0); for(i=0; i< len; i++) printf("ELEMENT %d IS %d\n", i, elems[i]);
中文处理
中文字符的处理每每是让人比较头疼的事情,非凡是使用Java语言开发的软件,在JNI这个问题更加突出。因为Java中全部的字符都是Unicode编码,可是在本地方法中,例如用VC编写的程序,假如没有非凡的定义通常都没有使用Unicode的编码方式。为了让本地方法可以访问Java中定义的中文字符及Java访问本地方法产生的中文字符串,我定义了两个方法用来作相互转换。
· 方法一,将Java中文字符串转为本地字符串
/** 第一个参数是虚拟机的环境指针第二个参数为待转换的Java字符串定义第三个参数是本地存储转换后字符串的内存块第三个参数是内存块的大小 */ int JStringToChar(JNIEnv *env, jstring str, LPTSTR desc, int desc_len) { int len = 0; if(desc==NULLstr==NULL) return -1; //在VC中wchar_t是用来存储宽字节字符(UNICODE)的数据类型 wchar_t *w_buffer = new wchar_t[1024]; ZeroMemory(w_buffer,1024*sizeof(wchar_t)); //使用GetStringChars而不是GetStringUTFChars wcscpy(w_buffer,env->GetStringChars(str,0)); env->ReleaseStringChars(str,w_buffer); ZeroMemory(desc,desc_len); //调用字符编码转换函数(Win32 API)将UNICODE转为ASCII编码格式字符串 //关于函数WideCharToMultiByte的使用请参考MSDN len = WideCharToMultiByte(CP_ACP,0,w_buffer,1024,desc,desc_len,NULL,NULL); //len = wcslen(w_buffer); if(len>0 && len
· 方法二,将C的字符串转为Java能识别的Unicode字符串
jstring NewJString(JNIEnv* env,LPCTSTR str) { if(!env !str) return 0; int slen = strlen(str); jchar* buffer = new jchar[slen]; int len = MultiByteToWideChar(CP_ACP,0,str,strlen(str),buffer,slen); if(len>0 && len < slen) buffer[len]=0; jstring js = env->NewString(buffer,len); delete [] buffer; return js; }
异常
因为调用了Java的方法,所以不免产生操做的异常信息。这些异常没有办法经过C++自己的异常处理机制来捕捉到,但JNI能够经过一些函数来获取Java中抛出的异常信息。以前咱们在Demo类中定义了一个方法throwExcp,下面将访问该方法并捕捉其抛出来的异常信息,代码以下:
/** 假设咱们已经构造了一个Demo的实例obj,其类定义为cls */ jthrowable excp = 0;/* 异常信息定义 */ jmethodID mid=(*env)->GetMethodID(env,cls,"throwExcp","()V"); /*假如mid为0表示获取方法定义失败*/ jstring msg = (*env)-> CallVoidMethod(env, obj, mid); /* 在调用该方法后会有一个IllegalAccessException的异常抛出 */ excp = (*env)->ExceptionOccurred(env); if(excp){ (*env)->ExceptionClear(env); //经过访问excp来获取具体异常信息 /* 在Java中,大部分的异常信息都是扩展类java.lang.Exception,所以能够访问excp的toString 或者getMessage来获取异常信息的内容。访问这两个方法同前面讲到的如何访问类的方法是相同的。 */ }
线程和同步访问
有些时候须要使用多线程的方式来访问Java的方法。咱们知道一个Java虚拟机是很是消耗系统的内存资源,差很少每一个虚拟机须要内存大约在20MB左右。为了节省资源要求每一个线程使用的是同一个虚拟机,这样在整个的JNI程序中只须要初始化一个虚拟机就能够了。全部人都是这样想的,可是一旦子线程访问主线程建立的虚拟机环境变量,系统就会出现错误对话框,而后整个程序终止。
其实这里面涉及到两个概念,它们分别是虚拟机(JavaVM *jvm)和虚拟机环境(JNIEnv *env)。真正消耗大量系统资源的是jvm而不是env,jvm是答应多个线程访问的,可是env只能被建立它自己的线程所访问,并且每一个线程必须建立本身的虚拟机环境env。这时候会有人提出疑问,主线程在初始化虚拟机的时候就建立了虚拟机环境env。为了让子线程可以建立本身的env,JNI提供了两个函数:AttachCurrentThread和DetachCurrentThread。下面代码就是子线程访问Java方法的框架:
DWord WINAPI ThreadProc(PVOID dwParam) { JavaVM jvm = (JavaVM*)dwParam;/* 将虚拟机经过参数传入 */ JNIEnv* env; (*jvm)-> AttachCurrentThread(jvm, (void**)&env, NULL); ......... (*jvm)-> DetachCurrentThread(jvm); }
时间
关于时间的话题是我在实际开发中碰到的一个问题。当要发布使用了JNI的程序时,并不必定要求客户要安装一个Java运行环境,由于能够在安装程序中打包这个运行环境。为了让打包程序利于下载,这个包要比较小,所以要去除JRE(Java运行环境)中一些没必要要的文件。可是假如程序中用到Java中的日历类型,例如java.util.Calendar等,那么有个文件必定不能去掉,这个文件就是[JRE]\lib\tzmappings。它是一个时区映射文件,一旦没有该文件就会发现时间操做上常常出现与正确时间相差几个小时的状况。下面是打包JRE中必不可少的文件列表(以Windows环境为例),其中[JRE]为运行环境的目录,同时这些文件之间的相对路径不能变。
文件名目录 hpi.dll [JRE]\bin ioser12.dll [JRE]\bin java.dll [JRE]\bin net.dll [JRE]\bin verify.dll [JRE]\bin zip.dll [JRE]\bin jvm.dll [JRE]\bin\classic rt.jar [JRE]\lib tzmappings [JRE]\lib
因为rt.jar有差很少10MB,可是其中有很大一部分文件并不须要,能够根据实际的应用状况进行删除。例如程序假如没有用到Java Swing,就能够把涉及到Swing的文件都删除后从新打包。