JNI 入門(一):從Hello World開始
前言
最近在學習JNI的相關知識,即Java Native Interface,它提供了若干API使得Java和C/C++的通訊成為可能。我們知道,Java程式碼運行於Java虛擬機器中,獨立於某個平臺,這也是Java的可移植性的優點。而C/C++程式碼運行於Windows或Linux平臺。為了實現Java和其他程式碼的互動,JNI應運而生。最簡單的就是,就是你在java中宣告一個方法,但方法的具體實現是由C/C++程式碼來實現的,然後生成dll庫或者so庫,java層通過引入這個庫而呼叫這個Native方法,也就是我們在Android中會遇到的Native方法。
JNI的優點
1、JNI能呼叫本地作業系統所提供的本地方法,呼叫系統級的介面。(dll庫是windows平臺,而so庫是Linux平臺)
2、對於一些以前用C/C++實現過的庫,可以用JNI來進行呼叫,而不用重新在Java層實現一次,節省時間。
3、對於某些特殊場景,需要高效率地執行程式碼,比如圖形渲染的過程,這時候顯然使用C/C++能極大提高執行速度。
JNI的缺點
使用了JNI與本地方法互動之後,犧牲了Java的可移植性這一優點,因為windwos和linux是不同的形式的庫,必須在具體平臺下重新編譯。
筆者的環境平臺
在開始之前,先說一下筆者所使用的IDE以及平臺
1、Windows 10作業系統
2、IntelliJ IDEA Community Edition 2018.3.5 x64
3、Visual Studio 2017
4、JDK10
從Hello World開始
好了,說了一大堆,那麼就讓我們開始學習JNI,先從最簡單的Hello World開始,先讓程式碼跑起來,然後再繼續深化學習。
1、在Intellij新建一個java專案

Java專案結構
①NativeTest類,裡面僅聲明瞭一個Native方法:

NativeTest類.png
native關鍵字表示這個方法由native層來實現,我們在java層不需要關注它的實現。
②Test類,這裡聲明瞭main方法,並呼叫NativeTest的native方法。

Test類.png
2、為native方法所在的類生成相應的標頭檔案
上面的native方法在NativeTest類中,我們需要為此生成一個.h檔案,只有這樣我們才能把.h檔案的方法用C/C++程式碼去實現。那麼,怎麼生成一個.h檔案呢?JDK為我們提供了工具專門用於生成JNI所用的標頭檔案。
格式是: javac -h <directory> <source file>
<directory>表示生成的.h檔案所放置的位置。
<source file>表示待編譯的原始檔。
上面的命令需要在命令列中使用,我們可以用Intellij提供的Terminal去輸入,也可以使用windows的cmd去輸入,二者的效果是一樣的。為了方便起見,筆者這裡使用的是Terminal。
2.1、首先,我們在Terminal定位到NativeTest類所在的資料夾,即:

命令列1.png
2.2、然後,通過使用javac -h命令,編譯NativeTest類,並生成與這個類有關的.h檔案:

命令列2.png
注意:這裡的<directory>使用了".",表示在當前目錄直接生成.h檔案。<source file>這裡填入NativeTest.java,因為當前目錄下有NativeTest.java檔案,所以不需要新增額外的路徑了。
2.3、執行之後,會發現當前目錄下多了兩個檔案:NativeTest.class和com_jni_NativeTest.h檔案:

專案目錄結構2.png
所以我們知道,javac -h實際上做了兩部操作,對NativeTest.java進行編譯生成class檔案,然後再生成.h檔案。
3、新建一個windows桌面程式專案
這一步,在VS 2017中進行,新建一個專案如下圖所示:

VS專案建立.png
我們這裡選擇動態連結庫(DLL),因為我們需要這個dll庫供java層使用,這裡專案的位置可以儲存在任何一個地方。
3.1、專案建立完畢之後,我們把剛才生成的com_jni_NativeTest.h檔案複製到當前專案的檔案目錄下,如下圖:

複製檔案的路徑.png
然後在VS2017的“解決方法資源管理器”中,在“標頭檔案選項”,右鍵選擇新增已有項,選中當前目錄下剛才複製進來的com_jni_NativeTest.h檔案:

新增現有項.png
3.2、接下來,我們需要把一些額外的檔案,開啟jdk的安裝目錄(筆者jdk的安裝目錄為:C:\Program Files\Java\jdk-10),在include資料夾下,複製jni.h和include\win32內的jni_md.h 這兩個檔案到NativeCode專案,即剛才存放com_jni_NativeTest.h檔案的目錄,同時把這兩個檔案作為現有項新增到標頭檔案,步驟3.1的最後。
完成3.1和3.2之後,我們會看到有如下的標頭檔案結構:

標頭檔案目錄結構.png
3.3、開啟專案中的com_jni_NativeTest.h檔案,把頂部的#include<jni.h>改成#include"jni.h",這樣就不會報錯了。解釋一下為什麼要做這樣的改動:#include<>形式表示C/C++檔案編譯時,首先從編譯器的類庫路徑裡面尋找該標頭檔案;而#include""表示在當前檔案目錄下尋找標頭檔案。
3.4、關於com_jni_NativeTest.h檔案的補充說明。
我們開啟這個標頭檔案,觀察其內部結構:

標頭檔案內部結構.png
我們可以看到,該標頭檔案聲明瞭一個方法Java_com_jni_NativeTest_sayHello(JNIEnv ,jobject),該方法有兩個關鍵字分別為JNIEXPORT和JNICALL 表示這個方法是要從Java層被呼叫的。然後該函式有兩個形參: JNIEnv** 和 jobject 。這兩個引數是native方法自帶的引數。
JNIEnv *是一個函式指標,它指向一系列JNI提供的函式來進行資料操作。
jobject 表示呼叫這個函式的物件,因為在java層它是一個例項方法,所以實際上這個引數的作用類似於 this關鍵字。
4、新建一個c++檔案,實現.h檔案所宣告的函式
這裡筆者直接使用VS幫我們生成的NativeCode.cpp檔案進行操作:

C++檔案實現.png
這裡僅簡單輸出了hello world。
5、編譯生成DLL檔案
這裡要使用x64的Debug偵錯程式(預設是x86),點選上方的生成——>生成解決方案,可以觀察到控制檯輸出了資訊,並標明瞭生成的dll檔案所在的位置,我們前往該位置,一般在 /專案目錄/x64/debug/NativeCode.dll。我們複製這個庫檔案到Java專案內。在這裡,筆者在java專案跟目錄下,新建了一個native_libs的資料夾,把dll檔案複製到這裡:

Java專案結構2.png
6、載入DLL檔案,並執行Main函式
DLL檔案已經被新增到我們的Java專案了,接下來的操作就是要在JVM執行時載入它,以便我們後續呼叫native方法。我們在Test.java檔案作點修改:

Test類2.png
好了,到目前為之,所有的工作都已經做完,讓我們執行一下Main函式,看一下效果如何?

執行結果.png
如果你看到了上面的輸出,那麼恭喜你,你已經掌握了JNI呼叫的基本方法!
小結
上面詳述了實現一個簡單JNI呼叫的步驟,現在小結一下整個流程。
(1)在一個Java類中宣告native方法。
(2)利用javac -h命令以該類為原始檔生成一個.h檔案
(3)在C/C++檔案中實現該標頭檔案所宣告的方法
(4)編譯C/C++檔案,生成一個DLL或so檔案,把它新增到java專案中
(5)通過System.loadLibrary方法載入這個庫檔案
踩過的坑
下面談談筆者在學習過程中遇到的一些問題,避免各位讀者再度踩坑。
1、關於javac -h和javah命令
筆者在剛學習JNI的知識時,在生成.h檔案階段,在網上查的教程都是利用javah命令來進行操作。然而在筆者的電腦總提示"javah不是內部命令",然而javac是可以正常運作的,這說明並不是環境變量出了問題,這就有可能是jdk安裝目錄下壓根就沒有javah.exe,經過查詢,確實是沒有這個檔案,所以javah命令會執行失敗。
那麼問題來了,為什麼我的JDK沒有javah.exe呢?
經過查閱資料,原來在JDK10以上的JDK內部已經去除了javah命令,它的功能被整合進了javac -h命令內。然而在jdk8以下的jdk是有該命令的。
解決途徑:如果電腦安裝的是jdk10以及10以上,使用javac -h命令;而jdk8以及8以下的使用javah -jni命令。
2、javac命令的進一步說明
javac命令是編譯命令,將java原始檔編譯成class位元組碼檔案。我們可以在命令列輸入javac -help,瞭解它的使用方法:

javac.png
其基本語法為:javac <options> <source files>,其中<options>可以是多個,而且<source files>也可以是多個,這時候表示把多個原始檔同時編譯。所以生成.h檔案的命令格式為:javac -h <directory> <source files>
需要注意的是:如果少了<directory>選項(如果是當前目錄直接用" . "代替),就會報錯,筆者曾在這裡卡了很長時間。
這篇文章到這裡就結束了,希望對各位同學有所裨益:) 謝謝看到這裡的你。下一篇文章將會圍繞JNI的資料操作、函式操作部分進行詳細講解。