一般,在開發 JNI 程序時,咱們都會使用 IDE,例如 Eclipse , Android Studio , 這是由於工具簡化了開發的流程,提高了工做效率,可是卻讓咱們愈來愈看不清本質的東西。所以,本篇文章就不使用 IDE 來作一次 JNI 開發,這樣咱們就能夠對原生 JNI 有個更全面的瞭解。php
本文的例子是在 Ubuntu 16.04 下運行的。html
首先咱們新建一個項目目錄叫 JNIDemo
,而後進入這個目錄java
bxll:~$ mkdir JNIDemo
bxll:~$ cd JNIDemo/
複製代碼
在 JNIDemo
目錄下,新建一個叫 Hello.java
的文件,代碼內容以下linux
package com.bxll.jnidemo;
public class Hello {
static {
System.loadLibrary("hello_jni");
}
static native String helloFromJNI();
public static void main(String[] args) {
System.out.println(helloFromJNI());
}
}
複製代碼
首先,在靜態代碼塊中,經過 System.loadLibrary()
方法加載一個名爲 hello_jni
的庫,在 Linux
平臺下,這個庫的全名叫作 libhello_jni.so
,在 Windows
平臺下,這個庫的名字叫作 hello_jni.dll
。因爲我使用的是 Ubuntu
,所以一會編譯這個庫的名字就 必須 爲 libhello_jni.so
。c++
而後,定義了一個 native
方法 helloFromJNI()
,這個方法須要在動態庫中實現,這個稍後就會看到。shell
最後,在 main()
方法中,調用這個 native
方法,並輸出這個方法的返回結果。windows
在編譯 Hello.java
文件以前,咱們須要建立一個存放字節碼文件的目錄,這個目錄暫且就叫作 classes
吧oracle
bxll:~/JNIDemo$ mkdir classes
複製代碼
而後,咱們把編譯生成的字節碼文件輸出到這個目錄jvm
bxll:~/JNIDemo$ javac -d classes/ Hello.java
複製代碼
javac
的-d
參數表示輸出的目錄,更多參數請參考 javac 。ide
在執行生成頭文件操做以前,咱們必需要搞清楚一個問題,那就是爲什麼要生成頭文件? 由於頭文件中聲明的函數和Java
文件中聲明的native
方法是一一對應的關係,虛擬機會自動幫咱們創建這層聯繫,這也稱之爲靜態註冊。
在生成頭文件以前,咱們須要建立一個存放頭文件的目錄jni
bxll:~/JNIDemo$ mkdir jni
複製代碼
而後把生成頭文件的目錄指定爲 jni
bxll:~/JNIDemo$ javah -classpath classes/ -d jni/ com.bxll.jnidemo.Hello
複製代碼
javah
的-classpath
參數指定字節碼的目錄,就是咱們剛纔編譯文件所指定的目錄,-d
參數指定頭文件生成的目錄,最後的com.bxll.jnidemo.Hello
指定字節碼文件的全路徑。更多的javah
命令參數請參考 javah 。
ok, 如今頭文件生成了,咱們如今來看看它的內容吧,簡化版的內容以下
// com_bxll_jnidemo_Hello.h
/* * Class: com_bxll_jnidemo_Hello * Method: helloFromJNI * Signature: ()Ljava/lang/String; */
JNIEXPORT jstring JNICALL Java_com_bxll_jnidemo_Hello_helloFromJNI (JNIEnv *, jclass);
複製代碼
咱們只須要關心這個函數原型(其餘都是C/C++相關的事),由於Java_com_bxll_jnidemo_Hello_helloFromJNI
這個函數就是對應的 Hello.java
中的 native
方法 helloFromJNI()
。
另外呢,咱們還能夠看到有三行註釋,第一個註釋 Class: com_bxll_jnidemo_Hello
指明瞭這個函數與哪一個 Java
類 相關,第二個註釋 helloFromJNI
指明是實現了哪一個 native
方法,第三個參數 ()Ljava/lang/String;
表明Java
類的native
方法在JNI
中的簽名。
JNIEXPORT
和JNICALL
都是宏,由於不一樣平臺調用動態庫中的方法有不一樣的規範,而這兩個宏就是爲了作兼容處理,在Linux
平臺,這兩個宏其實沒有什麼用,由於這兩個宏都是定義爲空的。
既然從頭文件中已經瞭解到函數原型,那麼就好實現了
// com_bxll_jnidemo_Hello.cpp
#include "com_bxll_jnidemo_Hello.h"
extern "C" JNIEXPORT jstring JNICALL Java_com_bxll_jnidemo_Hello_helloFromJNI (JNIEnv * env, jclass clazz) {
const char * str_hello = "Hello from C++";
return env->NewStringUTF(str_hello);
}
複製代碼
這裏涉及到 JNI
如何生成字符串的,這裏暫不作詳述。
既然咱們已經實現了底層函數,就須要將這些打包成庫,以方便 Java
層加載。咱們選擇把源文件打包成動態庫,可是在執行這個動做以前,咱們必須保證操做系統的 Java
開發環境已經部署穩當,最好也設置了 JAVA_HOME
環境變量,先看下個人 JAVA_HOME
環境變量
bxll:~/JNIDemo$ echo $JAVA_HOME
/usr/lib/jvm/java-8-openjdk-amd64/
複製代碼
那麼,如今來生成動態庫吧
bxll:~/JNIDemo$ g++ -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -fPIC -shared jni/com_bxll_jnidemo_Hello.cpp -o jni/libhello_jni.so
複製代碼
g++
的-I
參數指明JNI
頭文件的位置,-fPIC
表示變成成與位置無關的獨立代碼,-shared
表示編譯成動態庫,-o
指明生成動態庫的目錄以及名字,在Linux
系統下,動態庫的名字的形式爲libXXX.so
。
如今動態庫都生成了,那麼能夠運行程序了嗎?咱們試一下
bxll:~/JNIDemo$ java -classpath classes/ com.bxll.jnidemo.Hello
Exception in thread "main" java.lang.UnsatisfiedLinkError: no hello_jni in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at com.bxll.jnidemo.Hello.<clinit>(Hello.java:6)
複製代碼
java.lang.UnsatisfiedLinkError
就是告訴你動態庫沒有連接上,而且後面有說明緣由no hello_jni in java.library.path
,告訴你 java.library.path
沒有發現名爲 hello_jni
的動態庫,在 Linux
平臺下,也就是沒有發現 libhello_jni.so
庫。
既然咱們已經知道緣由是在 java.library.path
屬性所指定的目錄下沒有找到庫,那麼我能夠把生成的庫放到這個指定路徑下,這樣就能夠了吧。沒錯,確實能夠,可是這樣未免太麻煩,在 Linux
平臺下,能夠把庫的路徑加入到 LD_LIBRARY_PATH
環境變量中,程序也會在這個路徑下搜索庫。
因爲個人開發環境中暫時尚未定製本身的庫,所以 LD_LIBRARY_PATH
這個環境變量爲空,那麼如今咱們設置下
bxll:~/JNIDemo$export LD_LIBRARY_PATH=./jni/
複製代碼
咱們把庫的搜索路徑指定到了當前目錄下的 jni
目錄,由於剛纔咱們把動態庫輸出到這個目錄下。
那麼,如今再運行這個 Java
程序,你就會看到想要的效果
bxll:~/JNIDemo$ java -classpath classes/ com.bxll.jnidemo.Hello
Hello from C++
複製代碼
結果已經說明一切。
Linux平臺下靜態庫和動態庫