Java编程--关于JNI你应该知道的一切

出于效率的问题,很多情况下,我们需要在上层的Java代码中调用底层 C或C++实现,这时jni就可以大显身手了。jni(Java Native Interface)允许Java代码和其他语言写的代码进行交互,使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样 做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。


编程的步骤">使用JNI编程的步骤

在Java代码中使用native关键字声明一个本地方法 运行javah,获得包含该方法声明的C语言头文件(使用jni编程中的C函数名通常是相关于Java方法有一定的命名规则的,稍后会介绍,我们使用javah来帮助我们获得该方法名) 用C语言或C++实现我们所需要的功能 生成共享库文件,共享库文件可以是windows风格的.dll文件,也可以是UNIX风格的.so文件 为了确保虚拟机在第一次使用该类之前就会装载这个库,使用静态初始化块来加载

关于javah:在命令窗口中运行javah -h 命令我们可以看到javah的所有选项,这里简单介绍一下:

-classpath,用于装入类的路径 -d,输出目录 -o,输出文件, -d和-o只能使用其中之一 -jni,生成jni样式的头文件(默认)

编写第一个JNI程序

看到了上面的步骤我们来亲自实践一下,利用jni编写一个简单的hello world程序。 
1.首先,我们用Java代码编写一个本地方法hello:

?
1
2
3
4
5
6
7
package com.example.jnitest;
 
public class JniTest {
 
     public static native void hello();
 
}

2.接下来我们利用javah生成头文件,需要注意的是我们应该首先编译该工程得到.class文件。然后我们运行命令行来到工程目录/bin目录 下,输入命令 javah -classpath . com.example.jnitest.JniTest 得到头文件,如图: 
\
得到头文件的代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jnitest_JniTest */
 
#ifndef _Included_com_example_jnitest_JniTest
#define _Included_com_example_jnitest_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
  * Class:     com_example_jnitest_JniTest
  * Method:    hello
  * Signature: ()V
  */
JNIEXPORT void JNICALL Java_com_example_jnitest_JniTest_hello
   (JNIEnv *, jclass);
 
#ifdef __cplusplus
}
#endif
#endif

3.接下来我们要用C语言实现我们的程序,为了方便创建共享库文件,在这里我在VS2012中创建了一个控制台工程,并在应用程序设置中选择dll和空项目。如图: 
\
接着将刚刚得到的头文件添加进来并新建一个cpp文件来实现我们的程序。

?
1
#include "jni_hello.h"
 
JNIEXPORT void JNICALL Java_com_example_jnitest_JniTest_hello
     (JNIEnv *, jclass){
         printf( "Hello Jni!\n" );
}

注意问题,如果我们直接将头文件添加进来可能会找不到头文件jni.h,这时我们需要配置一下我们的工程,右键 工程–>属性–>VC++目录,选择包含目录–>编辑,将jni头文件的目录添加进来,一般需要添加的目录是jdk目录下的 include文件夹以及include/win32文件夹,如图: 
\
4.生成共享库文件,接下来我们只需要运行该工程即可在Debug文件夹下找到.dll文件,需要注意的是,vs直接生成的是32位的dll文件,如果你 的机器是64位就会报错Can’t load IA 32-bit .dll on a AMD 64-bit platform,这时我们就用配置一下自己的VS工程:生成–>配置管理器的活动解决方案平台,活动解决方案平台–>新建–>选择 x64即可 
\

接下来我们需要将我们的共享库添加进来:

首先我们调用System.out.println( System.getProperty(“java.library.path”));获得共享库的路径 接下来我们将dll文件拷贝到其中之一的路径中

5.加载共享库,调用native方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.jnitest;
 
public class Main {
 
     static {
         System.loadLibrary( "JniApplication" ); //静态初始化块加载库
     }
 
     public static void main(String[] args) {
         JniTest.hello();
     }
 
}

在控制台中输出结果: 
\

Jni中数据类型对应关系

了解了jni的简单用法,我们再来进行深入的学习:首先使jni中Java基本类型和C基本类型的对应关系 
\

除了基本的数据类型外,比较常用的当属字符串类型,Java中的String对应C中为jstring,我们可以使用如下方法将jstring类型和char *的相互转化

?
1
2
3
4
5
6
7
8
char *ff = "Hello Jni!" ;
         //创建一个jstring类型字符串
         jstring j_string = env->NewStringUTF(ff);
         //将jstring类型转化为char*
         const char * c_string = env->GetStringUTFChars(j_string, 0 );
         printf(c_string);
         //当我们不再使用字符串时,要将其释放
         env->ReleaseStringUTFChars(j_string, c_string);

访问Java中的域

使用C语言访问Java中的域和方法都要知道其Id,使用起来相对也比较复杂,在这里举一个简单的例子,假设我们的类People中有一个int类型的域age,即:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.jnitest;
 
public class People {
     private int age;
 
     public int getAge() {
         return age;
     }
 
     public void setAge( int age) {
         this .age = age;
     }
 
}

修改JniTest中的方法,这一次我们通过调用native方法使得age域变为原来的二倍:

?
1
2
3
4
5
6
package com.example.jnitest;
 
public class JniTest {
 
     public static native People changeAge(People people);
}

重新生成头文件后再cpp文件中编写我们的方法,每次访问Java中的域我们需要得到FieldId才可以,其步骤为: 
1. 获取隐式参数的类 
2. 获取域Id 
3. 访问域的值

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "jni_hello.h"
 
JNIEXPORT jobject JNICALL Java_com_example_jnitest_JniTest_changeAge
   (JNIEnv *env, jclass cl, jobject ob){
 
       jclass cl_people = env->GetObjectClass(ob);
       jfieldID c_ageId = env->GetFieldID(cl_people, "age" , "I" );
       jint c_age = env->GetIntField(ob,c_ageId);
       //打印修改之后的语句
       printf( "%d" , c_age);
       //修改age域
       env->SetIntField(ob, c_ageId, 2 *c_age);
       return ob;
}

env->GetFieldID方法中的第二个参数表示该域的名称,第三个参数表示该age域的类型为int,这个jni的编码签名有关,稍后会提到。接下来在main方法中调用:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.jnitest;
 
public class Main {
 
     static {
         System.loadLibrary( "JniApplication" );
     }
 
     public static void main(String[] args) {
         People people = new People();
         people.setAge( 5 );
         people = JniTest.changeAge(people);
         System.out.println( "changed::" +people.getAge());
     }
 
}

输出结果: 
\ 
可以看到,结果变为原来的2倍。对于访问静态域时只需更换env->GetIntField为env->GetStaticIntField即可。

访问Java中的方法

刚刚我们使用了访问域的方法去修改了域的值,下面我们在cpp文件中调用People类下的setAge方法修改age的值;调用Java的方法需要按照以下步骤:

获取隐式参数的类 获取方法Id 进行调用
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "jni_hello.h"
 
JNIEXPORT jobject JNICALL Java_com_example_jnitest_JniTest_changeAge
   (JNIEnv *env, jclass cl, jobject ob){
 
       jclass cl_people = env->GetObjectClass(ob);
       jfieldID c_ageId = env->GetFieldID(cl_people, "age" , "I" );
       jint c_age = env->GetIntField(ob,c_ageId);
       //第三个参数同样和编码签名有关
       jmethodID c_setAgeId = env->GetMethodID(cl_people, "setAge" , "(I)V" );
       //void表示了返回类型,若返回为int,则调用CallIntMethod
       env->CallVoidMethod(ob,c_setAgeId, 2 *c_age);
       return ob;
}

在main函数中调用,代码同上,结果如下: 
\ 
对于static方法的调用,我们只需要将调用时的方法更换就好,例如setAge方法为static的话,只需将 env->CallVoidMethod(ob,c_setAgeId,2*c_age);更换为 env->CallStaticVoidMethod(ob,c_setAgeId,2*c_age);即可。

Jni编码签名

在前面我们提到了env->GetFieldID方法中的第三个参数用编码签名来表示了该域的数据类型,现在来介绍一下jni中数据类的对应关系。

基本数据类型: 
B——byte, C——char, D——double, F——float, 
I——-int , J——long, S——short, V——–void , Z——boolean 数组类型,在开头加“[”来表示,如int[] 数组为“[I”;二维数组int[][] 即为“[[I” 类类型,以“L+完整包名+类名+;”表示其中所有的“.”换为“/”,如com.example.jnitest.People类表示为Lcom/example/jnitest/People;

关于方法的编码签名规则是,“(传入参数类型的编码签名)+返回参数类型的编码签名”,多个传入参数之间直接相接就好;例如我们的setAge方法中传入参数为int,返回为void,则表示为“(I)V”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值