標籤:ace 規範 定義 ica pre s函數 工作 自己 功能
什麼是JNI,怎麼使用
JNI——Java Native Interface,它是Java平台的一個特性(並不是Android系統特有的)。其實主要是定義了一些JNI函數,讓開發人員可以通過調用這些函數實現Java代碼調用C/C++的代碼,C/C++的代碼也可以調用Java的代碼,這樣就可以發揮各個語言的特點了。那麼怎麼使用JNI呢,一般情況下我們首先是將寫好的C/C++代碼編譯成對應平台的動態庫(windows一般是dll檔案,linux一般是so檔案等),這裡我們是針對Android平台,所以只討論so庫。由於JNI編程支援C和C++編程,這裡我們的栗子都是使用C++,對於C的版本可能會有些差異,但是主要的內容還是一致的,大家可以觸類旁通。
舉一個簡單的例子
這裡還是直接從代碼說起,這樣更加形象和直觀,也便於理解。今天使用的Java代碼如下:
public class AndroidJni { static{ System.loadLibrary("main"); } public native void dynamicLog(); public native void staticLog();}
這裡我們定義了兩個聲明為native的方法,並聲明了一塊靜態地區,在該靜態地區類載入名為libmain.so的庫,這裡我們說是libmain.so庫,但是載入的時候卻唯寫了“main”,其實大家只要知道這是約定的就可以了。
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是定義了很多我們使用的JNI函數和結構體,mylog.h是我自己定義的列印Android log的函數(功能和Java的Log類相同)。
這裡暫時不討論怎麼編譯成so庫以及so庫的一些規範。這裡假定將上面的C++程式編譯成了一個叫libmain.so的檔案。在Java層使用System.loadLibarary("main")方法將該so庫載入起來,使得dynamicLog()、staticLog()和對應的Java_com_github_songnick_jni_AndroidJni_staticLog()、nativeDynamicLog()兩個native方法連結起來,當然這部分工作都是由Java虛擬機器完成的,那麼具體是怎麼完成的,接下來將根據上面的代碼進行分析。
靜態註冊native方法
在上面的代碼中看到了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虛擬機器產生並在調用時傳遞進來的。
動態註冊
上面我們介紹了靜態註冊native方法的過程,就是Java層聲明的native方法和JNI函數是一一對應的,那麼有沒有方法讓Java層的native方法和任意的JNI函數連結起來,當然是可以的,這就得使用動態註冊的方法。接下來就看看如何?動態註冊的。
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結構體定義了以下函數:
DestroyJavaVMAttachCurrentThreadDetachCurrentThreadGetEnv
這裡我們使用了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)
第一個參數是Java層對應包含native方法的對象(這裡就是AndroidJni對象),通過調用JNIEnv對應的函數擷取class對象(FindClass函數的參數為需要擷取class對象的類描述符):
jclass clz = env->FindClass("com/test/jni/AndroidJni");
第二個參數是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}};
最後調用RegisterNative函數完成動態註冊:
env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));
JNIEnv結構體
上面提到JNIEnv這個結構體,它就老厲害了,指向一個函數表,該函數表指向一系列的JNI函數,我們通過調用這些JNI函數可以實現與Java層的互動,這裡簡單的看看幾個定義的函數:
..........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, ...)..........
這裡簡單的看看上面的四個函數,GetFieldID()函數是擷取Java對象中某個變數的ID,GetBooleanField()函數是根據變數的ID擷取資料類型為Boolean的變數。GetMethodID()函數是擷取Java對象中對應方法的ID,CallVoidMethod()根據methodID調用對應對象中的方法,並且該方法的傳回值為Void類型。通過這些函數我們可以實現調用Java層的代碼。
Android JNI編程—JNI基礎