JNI开发流程-JNI/NDK

Posted 请给我倒杯茶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JNI开发流程-JNI/NDK相关的知识,希望对你有一定的参考价值。

本文转载自:http://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/workflow.html

开发流程

JNI 全称是 Java Native Interface(Java 本地接口)单词首字母的缩写,本地接口就是指用 C 和 C++ 开发的接口。由于 JNI 是 JVM 规范中的一部份,因此可以将我们写的 JNI 程序在任何实现了 JNI 规范的 Java 虚拟机中运行。同时,这个特性使我们可以复用以前用 C/C++ 写的大量代码。

开发 JNI 程序会受到系统环境的限制,因为用 C/C++ 语言写出来的代码或模块,编译过程当中要依赖当前操作系统环境所提供的一些库函数,并和本地库链接在一起。而且编译后生成的二进制代码只能在本地操作系统环境下运行,因为不同的操作系统环境,有自己的本地库和 CPU 指令集,而且各个平台对标准 C/C++ 的规范和标准库函数实现方式也有所区别。这就造成使用了 JNI 接口的 JAVA 程序,不再像以前那样自由的跨平台。如果要实现跨平台,就必须将本地代码在不同的操作系统平台下编译出相应的动态库。

JNI 开发流程主要分为以下 6 步:

  • 编写声明了 native 方法的 Java 类
  • 将 Java 源代码编译成 class 字节码文件
  • 用 javah -jni 命令生成.h头文件(javah 是 jdk 自带的一个命令,-jni 参数表示将 class 中用native 声明的函数生成 JNI 规则的函数)
  • 用本地代码实现.h头文件中的函数
  • 将本地代码编译成动态库(Windows:\*.dll,linux/unix:\*.so,mac os x:\*.jnilib)
  • 拷贝动态库至 java.library.path 本地库搜索目录下,并运行 Java 程序

技术分享

通过上面的介绍,相信大家对 JNI 及开发流程有了一个整体的认识,下面通过一个 HelloWorld 的示例,再深入了解 JNI 开发的各个环节及注意事项。

HelloWorld

注意:这个案例用命令行的方式介绍开发流程,这样大家对 JNI 开发流程的印象会更加深刻,后面的案例都采用eclipse+cdt 来开发。

第一步,新建一个 HelloWorld.java 源文件


public class HelloWorld {

  public class HelloWorld {

    public static native String sayHello(String name); // 1.声明这是一个native函数,由本地代码实现

    public static void main(String[] args) {
        String text = sayHello("yangxin");  // 3.调用本地函数
        System.out.println(text);
    }

    static {
        System.loadLibrary("HelloWorld");   // 2.加载实现了native函数的动态库,只需要写动态库的名字
    }

}

第二步,用 javac 命令将.java源文件编译成.class字节码文件

注意:HelloWorld 放在 com.study.jnilearn 包下面

javac src/com/study/jnilearn/HelloWorld.java -d ./bin  

-d 表示将编译后的 class 文件放到指定的目录下,这里我把它放到和 src 同级的 bin 目录下。

第三步,用 javah -jni 命令,根据class字节码文件生成.h头文件(-jni 参数是可选的)

javah -jni -classpath ./bin -d ./jni com.study.jnilearn.HelloWorld  

默认生成的.h头文件名为:com_study_jnilearn_HelloWorld.h(包名+类名.h),也可以通过-o参数指定生成头文件名称:

javah -jni -classpath ./bin -o HelloWorld.h com.study.jnilearn.HelloWorld  

参数说明:

  • classpath:类搜索路径,这里表示从当前的 bin 目录下查找
  • d:将生成的头文件放到当前的 jni 目录下
  • o: 指定生成的头文件名称,默认以类全路径名生成(包名+类名.h)

注意:-d-o只能使用其中一个参数。

第四步,用本地代码实现.h头文件中的函数

  • com_study_jnilearn_HelloWorld.h
/* DO NOT EDIT THIS FILE - it is machine generated */  
#include <jni.h>  
/* Header for class com_study_jnilearn_HelloWorld */  

#ifndef _Included_com_study_jnilearn_HelloWorld  
#define _Included_com_study_jnilearn_HelloWorld  
#ifdef __cplusplus  
extern "C" {  
#endif  
/* 
 * Class:     com_study_jnilearn_HelloWorld 
 * Method:    sayHello 
 * Signature: (Ljava/lang/String;)Ljava/lang/String; 
 */  
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello  
  (JNIEnv *, jclass, jstring);  

#ifdef __cplusplus  
}  
#endif  
#endif  
  • HelloWorld.c
// HelloWorld.c  

#include "com_study_jnilearn_HelloWorld.h"  

#ifdef __cplusplus  
extern "C"  
{  
#endif  

/* 
 * Class:     com_study_jnilearn_HelloWorld 
 * Method:    sayHello 
 * Signature: (Ljava/lang/String;)Ljava/lang/String; 
 */  
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello(  
        JNIEnv *env, jclass cls, jstring j_str)  
{  
    const char *c_str = NULL;  
    char buff[128] = { 0 };  
    c_str = (*env)->GetStringUTFChars(env, j_str, NULL);  
    if (c_str == NULL)  
    {  
        printf("out of memory.\n");  
        return NULL;  
    }  
    (*env)->ReleaseStringUTFChars(env, j_str, c_str);  
    printf("Java Str:%s\n", c_str);  
    sprintf(buff, "hello %s", c_str);  
    return (*env)->NewStringUTF(env, buff);  
}  

#ifdef __cplusplus  
}  
#endif  

第五步,将 C/C++ 代码编译成本地动态库文件动态库文件名命名规则:lib+动态库文件名+后缀(操作系统不一样,后缀名也不一样)如:

  • Mac OS X : libHelloWorld.jnilib
  • Windows :HelloWorld.dll(不需要 lib 前缀)
  • Linux/Unix:libHelloWorld.so

1.Mac OS X

gcc -dynamiclib -o /Users/yangxin/Library/Java/Extensions/libHelloWorld.jnilib jni/HelloWorld.c -framework JavaVM -I/$JAVA_HOME/include -I/$JAVA_HOME/include/darwin  

$JAVA_HOME目录在:/Library/Java/JavaVirtualMachines/jdk1.7.0_21.jdk/Contents/Home (可根据具体情况自己设置)

参数选项说明:

  • -dynamiclib:表示编译成动态链接库
  • -o:指定动态链接库编译后生成的路径及文件名
  • -framework JavaVM -I:编译 JNI 需要用到 JVM 的头文件(jni.h),第一个目录是平台无关的,第二个目录是与操作系统平台相关的头文件

2.Windows (以 Windows7 下 VS2012 为例)

开始菜单-->所有程序-->Microsoft Visual Studio 2012-->打开 VS2012 X64 本机工具命令提示,用cl命令编译成dll动态库:

cl -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -LD HelloWorld.c -FeHelloWorld.dll   

参数选项说明:

  • -I :和 mac os x 一样,包含编译 JNI 必要的头文件
  • -LD:标识将指定的文件编译成动态链接库
  • -Fe:指定编译后生成的动态链接库的路径及文件名

技术分享

3.Linux/Unix

gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -shared HelloWorld.c -o libHelloWorld.so  

参数说明:

  • -I: 包含编译JNI必要的头文件
  • -fPIC: 编译成与位置无关的独立代码
  • -shared:编译成动态库
  • -o: 指定编译后动态库生成的路径和文件名

第六步,运行 Java 程序

Java 在调用 native (本地)方法之前,需要先加载动态库。如果在未加载动态之前就调用 native 方法,会抛出找不到动态链接库文件的异常。如下所示:

Exception in thread "main" java.lang.UnsatisfiedLinkError: com.study.jnilearn.HelloWorld.sayHello(Ljava/lang/String;)Ljava/lang/String;  
    at com.study.jnilearn.HelloWorld.sayHello(Native Method)  
    at com.study.jnilearn.HelloWorld.main(HelloWorld.java:9)  

一般在类的静态(static)代码块中加载动态库最合适,因为在创建类的实例时,类会被 ClassLoader 先加载到虚拟机,随后立马调用类的 static 静态代码块。这时再去调用 native 方法就万无一失了。加载动态库的两种方式:

System.loadLibrary("HelloWorld");  
System.load("/Users/yangxin/Desktop/libHelloWorld.jnilib"); 

方式1:只需要指定动态库的名字即可,不需要加lib前缀,也不要加.so.dll.jnilib后缀

方式2:指定动态库的绝对路径名,需要加上前缀和后缀

如果使用方式1,java 会去 java.library.path 系统属性指定的目录下查找动态库文件,如果没有找到会抛出java.lang.UnsatisfiedLinkError 异常。

Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorld2 in java.library.path  
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)  
    at java.lang.Runtime.loadLibrary0(Runtime.java:845)  
    at java.lang.System.loadLibrary(System.java:1084)  
    at com.study.jnilearn.HelloWorld.<clinit>(HelloWorld.java:13)  

大家从异常中可以看出来,他是在 java.library.path 中查找该名称对应的动态库,如果在 Mac 下找libHelloWorld.jnilib 文件,linux 下找 libHelloWorld.so 文件,Windows 下找 libHelloWorld.dll 文件,可以通过调用 System.getProperties("java.library.path")方法获取查找的目录列表,下面是我本机mac os x 系统下的查找目录:

String libraryDirs = System.getProperty("java.library.path");  
System.out.println(libraryDirs);  
// 输出结果如下:  
/Users/yangxin/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:

有两种方式可以让 Java 从 java.library.path 找到动态链接库文件,聪明的你应该已经想到了。

方式1:将动态链接库拷贝到java.library.path目录下

方式2:给 jvm 添加“-Djava.library.path=动态链接库搜索目录”参数,指定系统属性 java.library.path 的值 java -Djava.library.path=/Users/yangxin/Desktop Linux/Unix 环境下可以通过设置 LD_LIBRARY_PATH 环境变量,指定库的搜索目录。

运行写好的 Java 程序了,结果如下:

yangxin-MacBook-Pro:JNILearn yangxin$ java -classpath ./bin com.study.jnilearn.HelloWorld  
Java Str:yangxin  
hello yangxin  

如果没有将动态库拷贝到本地库搜索目录下,执行java命令,可通过添加系统属性 java.library.path 来指定动态库的目录,如下所示:

yangxin-MacBook-Pro:JNILearn yangxin$ java -Djava.library.path=/Users/yangxin/Desktop -classpath ./bin com.study.jnilearn.HelloWorld  
Java Str:yangxin  
hello yangxin  
 

以上是关于JNI开发流程-JNI/NDK的主要内容,如果未能解决你的问题,请参考以下文章

JNI/NDK开发指南

JNI/NDK开发指南——JNI异常处理

JNI/NDK开发指南——JNI局部引用全局引用和弱全局引用

JNI/NDK开发指南——JNI局部引用全局引用和弱全局引用

AS2.2使用CMake方式进行JNI/NDK开发

JNI/NDK开发指南(开山篇)