天天看點

JNI程式設計基礎(一)

jni-java native interface,是java平台提供的一個特性,通過編寫jni函數實作java代碼調用c/c++代碼以及c/c++代碼調用java代碼的作用。進而達到利用不同語言的特點。為什麼需要在java中調用c/c++代碼,在我看來最主要有以下三點:

c/c++代碼相比java有着更高的性能

c/c++代碼更難被反編譯,有更好的安全性

通過jni函數可以繞開jvm的限制,完成一些在java層面實作不了的功能。典型的例子就是android熱修複架構andfix

既然要實作c/c++和java代碼之間的互動,那麼jvm就必須提供一整套的機制來實作互相之間的轉換,具體來說涉及到以下三個方面:

jni函數的注冊

jni層面和java層面的資料結構對照

描述符-用于描述類名或者資料類型

1.jni函數的注冊

所謂jni函數的注冊就是jvm能夠準确的找到對應的jni函數,并将其連結到主程式。注冊分為動态注冊和靜态注冊,接下來通過一個例子來說明如何實作jni函數的靜态和動态注冊。

1.例子

這是一個普通的java類,類中申明了兩個native函數,dynamiclog和staticlog。native關鍵字告訴jvm,兩個函數是通過jni實作的,那麼在哪裡去找這兩個函數jni實作呢?注意,在這個類初始化的時候加載一個庫叫做main。沒錯,jvm就是會去main(如果是linux平台,這個庫就是libmain.so)這個庫中去找對應的函數。對應的c++代碼如下:

這裡引用了兩個頭檔案,jni.h和mylog.h,其中jni.h是定義

1.靜态注冊

在上面的代碼中看到了jniexport和jnicall關鍵字,這兩個關鍵字是兩個宏定義,他主要的作用就是說明該函數為jni函數,在java虛拟機加載的時候會連結對應的native方法,在androidjni.java的類中聲明了staticlog()為native方法,他對應的jni函數就是java_com_github_songnick_jni_androidjni_staticlog(),那麼是怎麼連結的呢,在java虛拟機加載so庫時,如果發現含有上面兩個宏定義的函數時就會連結到對應java層的native方法,那麼怎麼知道對應java中的哪個類的哪個native方法呢,我們仔細觀察jni函數名的構成其實是:java_pkgname_classname_nativemethodname,以java為字首,并且用“_”下劃線将包名、類名以及native方法名連接配接起來就是對應的jni函數了。一般情況下我們可以自己手動的去按照這個規則寫,但是如果native方法特别多,那麼還是有一定的工作量,并且在寫的過程中不小心就有可能寫錯,其實java給我們提供了javah的工具幫助生成相應的頭檔案。在生成的頭檔案中就是按照上面說的規則生成了對應的jni函數,我們在開發的時候直接copy過去就可以了。這裡上面的代碼為例,在androidstudio中編譯後,進入項目的目錄app/build/intermediates/classes/debug下,運作如下指令:

這裡-d指定生成.h檔案存放的目錄(如果沒有就會自動建立),com.github.songnick.jni.androidjni表示指定目錄下的class檔案。這裡簡單介紹一下生成的jni函數包含兩個固定的參數變量,分别是jnienv和jobject,其中jnienv後面會介紹,jobject就是目前與之連結的native方法隸屬的類對象(類似于java中的this)。這兩個變量都是java虛拟機生成并在調用時傳遞進來的。

2.動态注冊

上面我們介紹了靜态注冊native方法的過程,就是java層聲明的native方法和jni函數是一一對應的,那麼有沒有方法讓java層的native方法和任意的jni函數連結起來,當然是可以的,這就得使用動态注冊的方法。接下來就看看如何實作動态注冊的。

1) jni_onload函數

 當我們使用system.loadlibarary()方法加載so庫的時候,java虛拟機就會找到這個函數并調用該函數,是以可以在該函數中做一些初始化的動作,其實這個函數就是相當于activity中的oncreate()方法。該函數前面有三個關鍵字,分别是jniexport、jnicall和jint,其中jniexport和jnicall是兩個宏定義,用于指定該函數是jni函數。jint是jni定義的資料類型,因為java層和c/c++的資料類型或者對象不能直接互相的引用或者使用,jni層定義了自己的資料類型,用于銜接java層和jni層,至于這些資料類型我們在後面介紹。這裡的jint對應java的int資料類型,該函數傳回的int表示目前使用的jni的版本,其實類似于android系統的api版本一樣,不同的jni版本中定義的一些不同的jni函數。該函數會有兩個參數,其中*jvm為java虛拟機執行個體,javavm結構體定義了以下函數:

這裡我們使用了getenv函數擷取jnienv變量,上面的jni_onload函數中有如下代碼:

這裡調用了getenv函數擷取jnienv結構體指針,其實jnienv結構體是指向一個函數表的,該函數表指向了對應的jni函數,我們通過調用這些jni函數實作jni程式設計,在後面我們還會對其進行介紹。

擷取java對象,完成動态注冊

上面介紹了如何擷取jnienv結構體指針,得到這個結構體指針後我們就可以調用jnienv中的registernatives函數完成動态注冊native方法了。該方法如下:

第一個參數是java層對應包含native方法的對象(這裡就是androidjni對象),通過調用jnienv對應的函數擷取class對象(findclass函數的參數為需要擷取class對象的類描述符):

第二個參數是jninativemethod結構體指針,這裡的jninativemethod結構體是描述java層native方法的,它的定義如下:

第三個參數為注冊native方法的數量。一般會動态注冊多個native方法,首先會定義一個jninativemethod數組,然後将該數組指針作為registernative函數的參數傳入,是以這裡定義了如下的jninativemethod數組:

最後調用registernative函數完成動态注冊:

2jni資料結構

jnienv結構體

jnienv是一個jni環境結構體,結構體重維護了一系列的函數,通過這些環境函數可以實作與java層的互動。下圖是jnienv成員函數的一部分:

從上面羅列的幾個方法可以看出,通過jnienv我們可以輕易地擷取到一個java類中的域,方法并操作這些成員。

jni資料類型

雖然jni和java都包含很多相同的資料類型,但是其定義卻并不一樣,是以java的資料類型需要經過轉換才能在jni層面被操作。接下來就是java和jni資料類型的對照:

1)基礎類型

| java type| native type | description |

| --- | --- | --- |

| boolean | jboolean | unsigned 8 bits |

| byte | jbyte | signed 8 bits |

| char | jchar | unsigned 16 bits |

| short | jshort | signed 16 bits |

|int | jint | signed 32 bits |

|long | jlong | signed 64 bits |

|float | jfloat | 32 bits |

|double | jdouble| 64 bits |

|void | void | n/a |

2) 應用類型

3) 方法和變量的id

 當需要調用java中的某個方法的時候我們首先要擷取它的id,根據id調用jni函數擷取該方法,變量的擷取過程也是同樣的過程,這些id的結構體定義如下:

描述符

1.類描述符

 前面為了擷取java的androidjni對象,是通過調用findclass()函數擷取的,該函數參數隻有一個字元串參數,我們發現該字元串如下所示:

其實這個就是jni定義了對類的描述符,它的規則就是将”com.github.songnick.jni.androidjni”中的“.”用“/”代替。

2.方法描述符

 前面我們動态注冊native方法的時候結構體jninativemethod中含有方法描述符,就是确定native方法的參數和傳回值,我們這裡定義的dynamiclog()方法沒有參數,傳回值為空是以對應的描述符為:”()v”,括号類為參數,v表示傳回值為空。下面還是看看幾個栗子吧:

| method descriptor | java language type |

| --- | --- |

|“()ljava/lang/string;” | string f(); |

|“(iljava/lang/class;)j”| long f(int i, class c);|

|“([b)v” | string(byte[] bytes); |

上面的栗子我們看到方法的傳回類型和方法參數有引用類型以及boolean、int等基本資料類型,對于這些類型的描述符在下個部分介紹。這裡數組的描述符以”[“和對應的類型描述符來表述。對于二維數組以及三維數組則以”[[“和”[[[“表示:

|descriptor |java langauage type|

|“[[i” | int |

|“[[[d” | double[] |

3.資料類型描述符

 前面我們說了方法的描述符,那麼針對boolean、int等資料類型描述符是怎樣的呢,jni對基本資料類型的描述符定義如下:

| field desciptor | java language type |

| --- | ---- |

| z | boolean |

| b | byte |

| c | char |

|s | short |

|i | int |

|j | long |

|f | float |

|d | double |

對于引用類型描述符是以”l”開頭”;”結尾,示例如下所示:

| field desciptor | java language type |

| “ljava/lang/string;” | string |

|“[ljava/lang/object;” | object[] |