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

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

使用JNI编程的步骤

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

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

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

编写第一个JNI程序

看到了上面的步骤我们来亲自实践一下,利用jni编写一个简单的hello world程序。

1.首先,我们用Java代码编写一个本地方法hello:

package com.example.jnitest;

public class JniTest {

    public static native void hello();

}

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

得到头文件的代码如下:

/* 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文件来实现我们的程序。

#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方法:

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 *的相互转化

        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,即:

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域变为原来的二倍:

package com.example.jnitest;

public class JniTest {

    public static native People changeAge(People people);
}

重新生成头文件后再cpp文件中编写我们的方法,每次访问Java中的域我们需要得到FieldId才可以,其步骤为:

1. 获取隐式参数的类

2. 获取域Id

3. 访问域的值

#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方法中调用:

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的方法需要按照以下步骤:

  1. 获取隐式参数的类
  2. 获取方法Id
  3. 进行调用
#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”。

时间: 2024-11-11 09:32:09

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

Java编程最差实践常见问题详细说明(1)转

Java编程最差实践常见问题详细说明(1)转 原文地址:http://www.odi.ch/prog/design/newbies.php 每天在写Java程序, 其实里面有一些细节大家可能没怎么注意, 这不, 有人总结了一个我们编程中常见的问题. 虽然一般没有什么大问题, 但是最好别这样做. 另外这里提到的很多问题其实可以通过Findbugs(http://findbugs.sourceforge.net/ )来帮我们进行检查出来. 字符串连接误用  错误的写法: Java代码   Strin

Java JVM、JNI、Native Function Interface、Create New Process Native Function API Analysis

目录 1. JAVA JVM 2. Java JNI: Java Native Interface 3. Java Create New Process Native Function API Analysis In Linux 4. Java Create New Process Native Function API Analysis In Windows 1. JAVA JVM 0x1: JVM架构简介 JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种

异常笔记--java编程思想

开一个新的系列,主要记一些琐碎的重要的知识点,把书读薄才是目的...特点: 代码少,概念多... 1. 基本概念 异常是在当前环境下无法获得必要的信息来解决这个问题,所以就需要从当前环境跳出,就是抛出异常.抛出异常后发生的几件事: 1.在堆上创建异常对象. 2.当前的执行路径中止                                          3. 当前环境抛出异常对象的引用.                                         4. 异常处理机制接

《Java编程思想》第十三章 字符串

<Java编程思想>读书笔记 1.String作为方法的参数时,会复制一份引用,而该引用所指的对象其实一直待在单一的物理位置,从未动过. 2.显式地创建StringBuilder允许预先为他指定大小.如果知道字符串多长,可以预先指定StringBuilder的大小避免多次重新分配的冲突. 1 /** 2 * @author zlz099: 3 * @version 创建时间:2017年9月1日 下午4:03:59 4 */ 5 public class UsingStringBuilder {

Java编程练习之输出考试成绩的前三名

在慕课网学习的时候遇到了这样一个Java编程练习题,正好对所学习的Java基础知识检验一下: 请根据所学知识,编写一个Java程序,实现输出考试成绩的前三名 要求: 1考试成绩已保存在数组scores中,数组元素依次为89 , -23 , 64 , 91 , 119 , 52 , 73 2要求通过自定义方法来实现成绩排名并输出操作,将成绩数组作为参数传入 3要求判断成绩的有效性( 0-100 ),如果成绩无效,则忽略此成绩 我自己分析了一下这个程序的过程: (1)首先是定义一个包含整型数组参数的

《Java编程那点事儿》读书笔记(七)——多线程

1.继承Thread类 通过编写新的类继承Thread类可以实现多线程,其中线程的代码必须书写在run方法内部或者在run方法内部进行调用. public class NewThread extends Thread { private int ThreadNum; public NewThread(int ThreadNum){ this.ThreadNum = ThreadNum; } public void run(){ try{ for(int i = 0;i < 10;i ++){ T

Java编程思想【温故知新】

第一章:对象导论 1. 抽象过程(类与对象的关系) 类是一类对象的共同行为(成员函数)与状态(成员变量),对象是具体类的实例化.(Eg.人类是一个类,共同的行为:吃,状态:名字.) [类创建者需要考虑这件事情,回头看看这个概念四个字醍醐灌顶,每次创建这个类的时候,想一想这个类是需要什么成员函数与成员变量来满足单一职责的原则] 2. 每个对象都提供服务:程序设计本身的目标就是去创建能够提供服务来解决问题的一系列对象. 3. 被隐藏的具体实现:类创建者与客户端程序员使用者. 往往来说,每个程序员都是

Java编程思想 4th 第2章 一切都是对象

Java是基于C++的,但Java是一种更纯粹的面向对象程序设计语言,和C++不同的是,Java只支持面向对象编程,因此Java的编程风格也是纯OOP风格的,即一切都是类,所有事情在类对象中完成. 在Java中,使用引用来操纵对象,在Java编程思想的第四版中,使用的术语是"引用(reference)",之前有读过Java编程思想第三版,在第三版中,使用的术语是"句柄(handle)",事实上,我觉得第三版的术语"句柄"更加形象传神,就像你用一个

Java编程中“为了性能”尽量要做到的一些地方

下面是参考网络资源总结的一些在Java编程中尽可能要做到的一些地方. 1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 第一,控制资源的使用,通过线程同步来控制资源的并发访问: 第二,控制实例的产生,以达到节约资源的目的: 第三,控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信. 2. 尽量避免随意使用静态变量 要知道,当某个对象被定义为stataic变量所