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 Typ 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[]
最後更新:2017-04-23 11:30:38