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.例子
public class AndroidJni {
static{
System.loadLibrary("main");
}
public native void dynamicLog();
public native void staticLog();
}
這是一個普通的Java類,類中申明瞭兩個native函式,dynamicLog和staticLog。native關鍵字告訴JVM,兩個函式是通過JNI實現的,那麼在哪裡去找這兩個函式JNI實現呢?注意,在這個類初始化的時候載入一個庫叫做main。沒錯,JVM就是會去main(如果是Linux平臺,這個庫就是libmain.so)這個庫中去找對應的函式。對應的C++程式碼如下:
#include <jni.h>
#define LOG_TAG "main.cpp"
#include "mylog.h"
static void nativeDynamicLog(JNIEnv *evn, jobject obj){
LOGE("hell main");
}
JNIEXPORT void JNICALL Java_com_github_songnick_jni_AndroidJni_staticLog (JNIEnv *env, jobject obj)
{
LOGE("static register log ");
}
JNINativeMethod nativeMethod[] = {{"dynamicLog", "()V", (void*)nativeDynamicLog},};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
JNIEnv *env;
if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
LOGE("JNI_OnLoad comming");
jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni");
env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));
return JNI_VERSION_1_4;
}
這裡引用了兩個標頭檔案,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下,執行如下命令:
javah -d jni com.github.songnick.jni.AndroidJni
這裡-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結構體定義了以下函式:
DestroyJavaVM
AttachCurrentThread
DetachCurrentThread
GetEnv
這裡我們使用了GetEnv函式獲取JNIEnv變數,上面的JNI_OnLoad函式中有如下程式碼:
JNIEnv *env;
if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
這裡呼叫了GetEnv函式獲取JNIEnv結構體指標,其實JNIEnv結構體是指向一個函式表的,該函式表指向了對應的JNI函式,我們通過呼叫這些JNI函式實現JNI程式設計,在後面我們還會對其進行介紹。
獲取Java物件,完成動態註冊
上面介紹瞭如何獲取JNIEnv結構體指標,得到這個結構體指標後我們就可以呼叫JNIEnv中的RegisterNatives函式完成動態註冊native方法了。該方法如下:
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)11
第一個引數是Java層對應包含native方法的物件(這裡就是AndroidJni物件),通過呼叫JNIEnv對應的函式獲取class物件(FindClass函式的引數為需要獲取class物件的類描述符):
jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni");11
第二個引數是JNINativeMethod結構體指標,這裡的JNINativeMethod結構體是描述Java層native方法的,它的定義如下:
typedef struct {
const char* name;//Java層native方法的名字
const char* signature;//Java層native方法的描述符
void* fnPtr;//對應JNI函式的指標
} JNINativeMethod;
第三個引數為註冊native方法的數量。一般會動態註冊多個native方法,首先會定義一個JNINativeMethod陣列,然後將該陣列指標作為RegisterNative函式的引數傳入,所以這裡定義瞭如下的JNINativeMethod陣列:
JNINativeMethod nativeMethod[] = {{"dynamicLog", "()V", (void*)nativeDynamicLog}};11
最後呼叫RegisterNative函式完成動態註冊:
env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));11
2JNI資料結構
- JNIENV結構體
JNIENV是一個JNI環境結構體,結構體重維護了一系列的函式,通過這些環境函式可以實現與Java層的互動。下圖是JNIENV成員函式的一部分:
..........
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
jboolean GetBooleanField(jobject obj, jfieldID fieldID)
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
CallVoidMethod(jobject obj, jmethodID methodID, ...)
CallBooleanMethod(jobject obj, jmethodID methodID, ...)
..........
從上面羅列的幾個方法可以看出,通過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) 應用型別
jobject (all Java objects)
|
|-- jclass (java.lang.Class objects)
|-- jstring (java.lang.String objects)
|-- jarray (array)
| |--jobjectArray (object arrays)
| |--jbooleanArray (boolean arrays)
| |--jbyteArray (byte arrays)
| |--jcharArray (char arrays)
| |--jshortArray (short arrays)
| |--jintArray (int arrays)
| |--jlongArray (long arrays)
| |--jfloatArray (float arrays)
| |--jdoubleArray (double arrays)
|
|--jthrowable
3) 方法和變數的ID
當需要呼叫Java中的某個方法的時候我們首先要獲取它的ID,根據ID呼叫JNI函式獲取該方法,變數的獲取過程也是同樣的過程,這些ID的結構體定義如下:
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID *jfieldID; /* field IDs */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID *jmethodID; /* method IDs */
- 描述符
1.類描述符
前面為了獲取Java的AndroidJni物件,是通過呼叫FindClass()函式獲取的,該函式引數只有一個字串引數,我們發現該字串如下所示:
com/github/songnick/jni/AndroidJni11
其實這個就是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[] |
相關文章
- 程式設計基礎程式設計
- 併發程式設計——基礎概念(一)程式設計
- Socket程式設計基礎程式設計
- Go程式設計基礎Go程式設計
- Java程式設計基礎Java程式設計
- Shell程式設計-基礎程式設計
- C程式設計基礎C程式程式設計
- shell程式設計基礎程式設計
- Go 併發程式設計 - Goroutine 基礎 (一)Go程式設計
- 網路程式設計基礎-socket基礎程式設計
- Javascript基礎與物件導向基礎~第一講啥叫程式設計,啥叫程式設計師JavaScript物件程式設計師
- Java 基礎02Java程式設計基礎Java程式設計
- Java併發程式設計——基礎知識(一)Java程式設計
- PLC(一)可程式設計控制器基礎程式設計
- 程式設計之基礎:資料型別(一)程式設計資料型別
- linux下bluetooth程式設計(一)基礎概念Linux程式設計
- 程式設計基礎知識程式設計
- shell程式設計基礎二程式設計
- 【程式設計基礎】輸出程式設計
- 【socket程式設計基礎模板】程式設計
- 網路程式設計基礎程式設計
- QML程式設計 基礎 小白程式設計
- 【Java基礎】通用程式設計Java程式設計
- linux程式設計基礎Linux程式設計
- pl/sql程式設計基礎SQL程式設計
- python程式設計基礎Python程式設計
- 零基礎帶你吃掉JNI全家桶(一)
- OpenGL基礎圖形程式設計(四)基礎程式結構程式設計
- Golang併發程式設計基礎Golang程式設計
- 併發程式設計基礎(上)程式設計
- 併發程式設計基礎(下)程式設計
- JAVA網路程式設計基礎Java程式設計
- Java 基礎程式設計筆記Java程式設計筆記
- Java程式設計基礎33——JDBCJava程式設計JDBC
- React基礎:宣告式程式設計React程式設計
- python 程式設計基礎案例Python程式設計
- Java併發程式設計基礎Java程式設計
- c# windows程式設計基礎C#Windows程式設計