<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java筑基 - JNI到底是個啥

          共 6619字,需瀏覽 14分鐘

           ·

          2021-07-14 02:23

          本文介紹一下JNI(Java Native Interface以及它的使用。

          首先回顧一下jni的主要功能,從jdk1.1開始jni標準就成為了java平臺的一部分,它提供的一系列的API允許java和其他語言進行交互,實現了在java代碼中調用其他語言的函數。通過jni的調用,能夠實現這些功能:

          通常情況下我們一般使用jni用來調用c或c++中的代碼,在上一篇文章中我們用了下面的流程來描述了native方法的調用過程:

          Java Code -> JNI -> C/C++ Code

          但是準確的來說這一過程并不嚴謹,因為最終被執(zhí)行的不是原始的c/c++代碼,而是被編譯連接后的動態(tài)鏈接庫。因此我們將這個過程從單純的代碼調用層面上進行升級,將jni的調用過程提高到了jvm和操作系統的層面,來加點細節(jié)進行一下完善:

          看到這里,可能有的小伙伴就要提出疑問了,不是說java語言是跨平臺的嗎,這種與操作系統本地編譯的動態(tài)鏈接庫進行的交互,會不會使java失去跨平臺的可移植性?

          針對這一問題,大家可以回想一下以前安裝jdk的經歷,在官網的下載列表中提供了各個操作系統的不同版本jdk,例如windowslinuxmac os版本等等,在這些jdk中,針對不同系統有著不同的jvm實現。而java語言的跨平臺性恰好是和它底層的jvm密不可分的,正是依靠不同的操作系統下不同版本jvm的“翻譯”工作,才能使編譯后的字節(jié)碼在不同的平臺下暢通無阻的運行。

          在不同操作系統下,c/c++或其他代碼生成的動態(tài)鏈接庫也會有差異,例如在window平臺下會編譯為dll文件,在linux平臺下會編譯為so文件,在mac os下會編譯為jnilib文件。而不同平臺下的jvm,會“約定俗成”的去加載某個固定類型的動態(tài)鏈接庫文件,使得依賴于操作系統的功能可以被正常的調用,這一過程可以參考下面的圖來進行理解:

          在對jni的整體調用流程有了一定的了解后,對于它如何調用其他語言中的函數這一過程,你是否也會好奇它是怎樣實現的,下面我們就通過手寫一個java程序調用c++代碼的例子,來理解它的調用過程。

          1、準備java代碼

          首先定義一個包含了native方法的類如下,之后我們要使用這個類中的native方法通過jni調用c++編寫成的動態(tài)鏈接庫中的方法:

          public class JniTest {
              static{
                  System.loadLibrary("MyNativeDll");
              }

              public static native void callCppMethod();

              public static void main(String[] args) {
                  System.out.println("DLL path:"+System.getProperty("java.library.path"));
                  callCppMethod();
              }
          }

          在代碼中主要完成了以下工作:

          • 在靜態(tài)代碼塊中,調用loadLibrary方法加載本地的動態(tài)鏈接庫,參數為不包含擴展名的動態(tài)鏈接庫庫文件名。在window平臺下會加載dll文件,在linux平臺下會加載so文件,在mac os下會加載jnilib文件
          • 聲明了一個native方法,native關鍵字負責通知jvm這里調用方法的是本地方法,該方法在外部被定義
          • main方法中,打印加載dll文件的路徑,并調用本地方法

          2、生成頭文件

          在使用c/c++來實現本地方法時,需要先創(chuàng)建.h頭文件。簡單的來說,c/c++程序通常由頭文件(.h)和定義文件(.c.cpp)組成,頭文件包含了功能函數、數據接口的聲明,而定義文件用于書寫程序的實現。

          在jdk8中可以直接使用javac -h指令生成c/c++語言中的頭文件。如果你使用的是較早版本的jdk,需要在執(zhí)行javac編譯完成class文件后,再執(zhí)行javah -jni生成c/c++風格的頭文件(在jdk10的新特性中已經刪除了javah這一指令)。我們使用的jdk8簡化了這一步驟,使其可以一步完成,在命令行窗口下執(zhí)行命令:

          javac -h ./jni JniTest.java

          指令中使用 -h參數指定放置生成的頭文件的位置,最后的參數是java源文件的名稱。在這個過程中完成了兩件工作,首先生成class文件,其次在參數指定的目錄下生成頭文件。生成的頭文件com_cn_jni_JniTest.h內容如下:

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

          #ifndef _Included_com_cn_jni_JniTest
          #define _Included_com_cn_jni_JniTest
          #ifdef __cplusplus
          extern "C" {
          #endif
          /*
           * Class:     com_cn_jni_JniTest
           * Method:    callCppMethod
           * Signature: ()V
           */

          JNIEXPORT void JNICALL Java_com_cn_jni_JniTest_callCppMethod
            (JNIEnv *, jclass)
          ;

          #ifdef __cplusplus
          }
          #endif
          #endif

          生成的頭文件和大家熟悉的 java接口有些相似,只有函數的聲明而沒有具體實現。簡單的解釋一下頭文件中的代碼:

          • extern "C"告訴編譯器,這部分代碼使用C語言規(guī)則來進行編譯
          • JNIEXPORTJNICALLjni中定義的兩個宏,使用JNIEXPORT支持在外部程序代碼中調用該動態(tài)庫中的方法,使用JNICALL定義函數調用時參數的入棧出棧約定
          • 函數名稱由包名+類名+方法名組成,在該方法中有兩個參數,通過第一個參數JNIEnv *的對象可以調用jni.h中封裝好的大量函數 ,第二個參數代表著native方法的調用者,當java代碼中定義的native方法是靜態(tài)方法時這里的參數是jclass,非靜態(tài)方法的參數是jobject

          接下來我們創(chuàng)建一個cpp文件,引用頭文件并實現其中的函數,也就是native方法將要實際執(zhí)行的邏輯:

          #include "com_cn_jni_JniTest.h"
          #include <stdio.h>
           
          JNIEXPORT void JNICALL Java_com_cn_jni_JniTest_callCppMethod
            (JNIEnv *, jclass)
          {
              printf("Print From Cpp: \n");
              printf("I am a cpp method ! \n");
          }

          在方法的實現中加入簡單的printf打印語句,在完成方法的實現后,我們需要將上面的cpp文件編譯為動態(tài)鏈接庫,提供給java中的native方法調用,因此下面需要在window環(huán)境下安裝gcc環(huán)境。

          3、gcc環(huán)境安裝

          在window環(huán)境下,如果你不希望為了生成一個dll就去下載體積龐大的的Visual Studio的話,MinGW是一個不錯的選擇,簡單的說它就是一個windows版本下的gcc。那么估計有的同學又要問了,gcc是什么?簡單的來說就是linux系統下C/C++的編譯器,通過它可以將源代碼編譯成可執(zhí)行程序。首先從下面的網址下載mingw-get-setup的安裝程序:

          http://sourceforge.net/projects/mingw/  #32位
          https://sourceforge.net/projects/mingw-w64/  #64位

          需要注意,一定要按照系統位數安裝對應的版本,否則后面生成的dll在運行時就可能會因位數不匹配而報錯,我在實驗的過程中第一次就錯誤安裝了32位的MinGw,導致了在程序運行過程中報了下面錯誤:

          Exception in thread "main" java.lang.UnsatisfiedLinkError: 
          F:\Workspace20\unsafe-test\src\main\java\com\cn\jni\jni\MyNativeDll.dll: 
          Can't load IA 32-bit .dll on a AMD 64-bit platform

          安裝完成后,將MinGW\bin目錄加入系統環(huán)境變量PATH,輸入下面的指令測試gcc是否可以使用:

          gcc -v

          如果能夠正常輸出gcc的版本信息,說明gcc安裝成功:

          在測試的過程中發(fā)現,如果安裝的是64位的mingw,那么在安裝完成后gcc就已經直接可以可用。但是如果安裝的是32位的mingw,需要使用下面的命令單獨安裝gcc

          mingw-get install gcc

          gcc安裝完成后,如果還想安裝gdbmake等其他指令進行調試或編譯,同樣可以使用強大的mingw-get命令進行獨立安裝。

          4、生成動態(tài)鏈接庫

          gcc環(huán)境準備好的條件下,接下來使用下面的命令生成dll動態(tài)鏈接庫:

          gcc -m64 -Wl,--add-stdcall-alias -I"D:\Program Files\Java\jdk1.8.0_261\include" 
          -I"D:\Program Files\Java\jdk1.8.0_261\include\win32" 
          -shared -o MyNativeDll.dll JniTestImpl.cpp

          簡單的解釋一下各個參數的含義:

          • -m64 :將cpp代碼編譯為64位的應用程序
          • -Wl,--add-stdcall-alias-Wl表示將后面的參數傳遞給連接程序,參數--add-stdcall-alias表示帶有標準調用后綴@NN的符號會被剝掉后綴后導出
          • -I:指定頭文件的路徑,在生成的頭文件代碼中引入的jni.h就在這個目錄下
          • -shared:指定生成動態(tài)鏈接庫,如果不使用這個標志那么外部程序將無法連接
          • -o:指定目標的名稱,這里將生成的動態(tài)鏈接庫命名為MyNativeDll.dll
          • JniTestImpl.cpp:被編譯的源程序文件名

          在指令的執(zhí)行過程中,都做了什么事呢,可以參考下面這張圖:

          在執(zhí)行過程中,以.cpp源代碼和.h頭文件作為源文件,先進行了預處理、編譯、匯編的操作,圖中省略了這一階段產生的一些中間文件,編譯完成后生成的.o二進制文件相對重要,依賴這個文件,最終生成動態(tài)鏈接庫。

          在執(zhí)行了上面的指令后,就會在當前目錄下生成一個MyNativeDll.dll文件,再運行之前準備好的java代碼:

          程序報錯,這是因為在默認的載入庫文件的目錄下沒有找到我們的dll文件。有兩種方式可以解決:

          • 直接將dll文件拷貝到默認的加載目錄下,具體的路徑可以通過System.getProperty("java.library.path")獲取,該方法可能會獲得多個目錄,放在任意一個目錄下即可
          • 是在VM Option中修改啟動參數,指定dll的存放目錄:
          -Djava.library.path=F:\Workspace20\unsafe-test\src\main\java\com\cn\jni\jni

          再次執(zhí)行,輸出結果:

          DLL path:F:\Workspace20\unsafe-test\src\main\java\com\cn\jni\jni
          Print From Cpp:
          I am a cpp method !

          可以看到程序加載dll的路徑已經切換成了它的存放路徑,并且通過jni調用成功,輸出了在c++中的代碼邏輯。可以用下面的圖來總結上面實現jni調用的過程:

          在對jni的調用有了一個整體的了解后,如果大家對代理模式比較熟悉的話,也可以從代理模式的角度來理解jni,將jni調用過程中的各個角色帶入到代理模式中:

          • 代理角色:包含native方法的jni
          • 實現角色:c/c++或其他語言實現的動態(tài)鏈接庫
          • 客戶端:調用native方法的java類程序
          • 接口(抽象角色):在jni中接口這一角色的存在感相對薄弱,因為jni是跨語言的,所以說無法嚴格的定義一個接口并讓它同時應用于java和其他語言。但是通過生成的.h頭文件,在一定程度上實現了從接口規(guī)范上統一了java中native方法和其他語言中的函數

          以代理模式的概述圖來進行描述:

          上圖在標準代理模式的基礎上做了一些修改以便于理解,因為這里的接口只做規(guī)范約束作用,所以讓客戶端的調用過程跳過了接口,直接指向了代理角色,再由代理角色調用實現角色完成功能的調用。總的來說,jni起到了一個代理或中介的作用,與常見代理不同的是這里只做方法的調用,而不實現邏輯上的增強。通過這一模式,向java程序員隱藏了底層c/c++代碼的實現細節(jié),讓我們專注于業(yè)務代碼的編寫即可。

          總結

          在前面對native方法有了一定了解的基礎上,本文介紹了jni的相關知識。通過本文的學習,有助于我們:

          • 理解java的為何能夠做到跨平臺,以及依賴操作系統的底層操作是如何實現的
          • 了解native方法的調用過程,在必要時可以自己實現jni類接口調用
          • 回顧一些C/C++知識

          當然了,使用jni也會帶來一些缺點:

          • 當在某個操作系統下使用了jni標準,將本地代碼編譯生成了動態(tài)鏈接庫后,如果要將這個程序移植到其他操作系統,需要在新的平臺重新編譯代碼生成動態(tài)鏈接庫
          • 對其他語言的不正確使用可能會造成程序出現錯誤,例如之前提到的使用c語言進行內存操作時未及時回收內存可能引起的內存泄漏
          • 對其他語言的依賴過高,會提高了java和其他語言的耦合性,也提高了對項目代碼的維護成本
          瀏覽 52
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产女人高潮了视频 | 青娱乐福利| 欧洲成人免费视频 | 99综合97 | 日韩操逼内射 |