JNI,全稱(chēng)Java Native Interface,是用于讓運(yùn)行在JVM中的Java代碼和運(yùn)行在JVM外的Native代碼(主要是C或者C++)溝通的橋梁。代碼編寫(xiě)者即可以使用JNI從Java的程序中調(diào)用Native代碼,又可以從Native程序中調(diào)用Java代碼。這樣,編程人員可以將低階的代碼邏輯包裝到高階的程序框架中,獲得高性能高效率的同時(shí)保證了代碼框架的高抽象性。
在Android中,僅有以下類(lèi)庫(kù)是允許在JNI中使用的:
libc (C library) headers
libm (math library) headers
JNI interface headers
libz (Zlib compression) headers
liblog (Android logging) header
OpenGL ES 1.1 (3D graphics library) headers (since 1.6)
A Minimal set of headers for C++ support
JNI本身僅僅是一個(gè)把兩者融合的工具,作為編程者需要做的,就是在Java代碼和Native代碼中按照固定的格式告訴JNI如何調(diào)用對(duì)方。在Android中,有兩種方式可以調(diào)用JNI,一種是Google release的專(zhuān)門(mén)針對(duì)Android Native開(kāi)發(fā)的工具包,叫做NDK。去Android網(wǎng)站上下載該工具包后,就可以通過(guò)閱讀里面的文檔來(lái)setup一個(gè)新的包含Native代碼的工程,創(chuàng)建自己的Android.mk文件,編譯等等;另一種是完整的源碼編譯環(huán)境 ,也就是通過(guò)git從官方網(wǎng)站獲取完全的Android源代碼平臺(tái)。這個(gè)平臺(tái)中提供有基于make的編譯系統(tǒng)。更多細(xì)節(jié)請(qǐng)參考這里。不管選擇以上兩種方法的哪一個(gè),都必須編寫(xiě)自己的Android.mk文件,有關(guān)該文件的編寫(xiě)請(qǐng)參考相關(guān)文檔。
下面通過(guò)一個(gè)簡(jiǎn)單的使用例子來(lái)講解JNI。Android給C和C++提供的是兩套不同的Native API,本文僅以C++舉例說(shuō)明。假設(shè)這么一個(gè)需求,Java代碼需要打印一個(gè)字符串,而該字符串需要Native代碼計(jì)算生成。對(duì)應(yīng)的JNI流程是這樣的:
1. 在準(zhǔn)備打印字符串的Android類(lèi)中,添加兩段代碼。
第一段是:
private native String getPrintStr();
這一行代碼的目的是告訴JNI,這個(gè)Java文件中有這么一個(gè)函數(shù),該函數(shù)是在Native代碼中執(zhí)行的,Native代碼會(huì)返回一個(gè)字符串供Java代碼來(lái)輸出。
第二段是:
try {System.loadLibrary(“LIBNAME” }
catch (UnsatisfiedLinkError ule) {Log.e(TAG, “Could not load native library”);}
這兩行代碼是告訴JNI,你需要找的所有Native函數(shù)都在libLIBNAME.so這個(gè)動(dòng)態(tài)庫(kù)中。注意JNI會(huì)自動(dòng)補(bǔ)全lib和so給LIBNAME,你只需要提供LIBNAME給loadLibrary就行了。在最后執(zhí)行的時(shí)候,JNI會(huì)先找到這個(gè)動(dòng)態(tài)庫(kù),然后找里面的OnLoad函數(shù),具體注冊(cè)流程由OnLoad函數(shù)接管。
關(guān)于如何確定這個(gè)LIBNAME,和如何定義OnLoad函數(shù),下面就會(huì)講。
2. 上面的第一步是告訴JNI,java代碼需要和Native代碼交互,同時(shí)把在哪里找,找什么都通知了。接下來(lái)的事情就由Native端接管。如果把上面的getPrintString函數(shù)申明比作原型,那么本地代碼中的具體函數(shù)定義就應(yīng)該和該原型匹配,JNI才能知道具體在哪里執(zhí)行代碼。具體來(lái)說(shuō),應(yīng)該有一個(gè)對(duì)應(yīng)的Native函數(shù),有和Java中定義的函數(shù)同樣的參數(shù)列表以及返回值。另外,還需要有某種機(jī)制讓JNI將兩者相互映射,方便參數(shù)和返回值的傳遞。在老版的JNI中,這是通過(guò)丑陋的命名匹配實(shí)現(xiàn)的,比如說(shuō)在Java中定義的函數(shù)名是getPrintStr, 該函數(shù)屬于package java.come.android.xxx,那么中對(duì)應(yīng)Native代碼中的函數(shù)名就應(yīng)該是Java_com_android_xxx_getPrintStr。這樣給開(kāi)發(fā)人員帶來(lái)了很多不便??梢杂胘avah命令來(lái)生成對(duì)應(yīng)Java code中定義函數(shù)的Native code版本header文件,從中得知傳統(tǒng)的匹配方法是如何做的。具體過(guò)程如下:
通過(guò)SDK的方式編譯Java代碼。
找到Eclipse的工程目錄,進(jìn)入bin目錄下。這里是編譯出的java文件所對(duì)應(yīng)的class文件所在。
假設(shè)包括Native函數(shù)調(diào)用的java文件屬于com.android.xxx package,名字叫test.java,那么在bin下執(zhí)行javah -jni com.android.xxx.test
執(zhí)行完后,可以看到一個(gè)新生成的header文件,名字為com_android_xxx_test.h。打開(kāi)后會(huì)發(fā)現(xiàn)已經(jīng)有一個(gè)函數(shù)申明,函數(shù)名為java_com_android_xxx_test_getPrintStr。這個(gè)名字就包括了該函數(shù)所對(duì)應(yīng)Java版本所在的包,文件以及名稱(chēng)。這就是JNI傳統(tǒng)的確定名字的方法。
值得注意的是,header文件中不僅包含了基于函數(shù)名的映射信息,還包含了另一個(gè)重要信息,就是signature。一個(gè)函數(shù)的signature是一個(gè)字符串,描述了這個(gè)函數(shù)的參數(shù)和返回值。其中”()” 中的字符表示參數(shù),后面的則代表返回值。例如”()V” 就表示void Func(); “(II)V” 表示 void Func(int, int); 數(shù)組則以”["開(kāi)始,用兩個(gè)字符表示。
具體的每一個(gè)字符的對(duì)應(yīng)關(guān)系如下:
字符
Java類(lèi)型
C類(lèi)型
V
void
void
I
jint
int
Z
jboolean
boolean
J
jlong
long
D
jdouble
double
F
jfloat
float
B
jbyte
byte
C
jchar
char
S
jshort
short
上面的都是基本類(lèi)型。如果Java函數(shù)的參數(shù)是class,則以"L"開(kāi)頭,以";"結(jié)尾,中間是用"/" 隔開(kāi)的包及類(lèi)名。而其對(duì)應(yīng)的C函數(shù)名的參數(shù)則為jobject。 一個(gè)例外是String類(lèi),其對(duì)應(yīng)的類(lèi)為jstring。舉例:
Ljava/lang/String; String jstring
Ljava/net/Socket; Socket jobject
如果JAVA函數(shù)位于一個(gè)嵌入類(lèi),則用$作為類(lèi)名間的分隔符。例如 "(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z"
這個(gè)signature非常重要,是下面要介紹的新版命名匹配方法的關(guān)鍵點(diǎn)之一。所以,即使傳統(tǒng)的命名匹配已經(jīng)不再使用,javah這一步操作還是必須的,因?yàn)榭梢詮闹械玫絁ava代碼中需要Native執(zhí)行的函數(shù)的簽名,以供后面使用。
3. 在新版(版本號(hào)大于1.4)的JNI中,Android提供了另一個(gè)機(jī)制來(lái)解決命名匹配問(wèn)題,那就是JNI_OnLoad。正如前面所述,每一次JNI執(zhí)行Native代碼,都是通過(guò)調(diào)用JNI_OnLoad實(shí)現(xiàn)的。下面的代碼是針對(duì)本例的OnLoad代碼:
/* Returns the JNI version on success, -1 on failure.
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed");
goto bail;
}
assert(env != NULL);
if (!register_Test(env)) {
LOGE("ERROR: Test native registration failed");
goto bail;
}
/* success -- return valid version number */
result = JNI_VERSION_1_4;
bail: return result;
}
分析這個(gè)函數(shù)。首先,OnLoad通過(guò)GetEnv函數(shù)獲取JNI的環(huán)境對(duì)象,然后通過(guò)register_Test來(lái)注冊(cè)Native函數(shù)。register_Test的實(shí)現(xiàn)如下:
int register_Test(JNIEnv *env) {
const char* const ClassPathName = "com/android/xxx/test";
return registerNativeMethods(env, ClassPathName, TestMethods,
sizeof(TestMethods) / sizeof(TestMethods[0]));
}
在這里,ClassPathName是Java類(lèi)的全名,包括package的全名。只是用 “/” 代替 ”.” 。然后我們把類(lèi)名以及TestMethods這個(gè)參數(shù)一同送到registerNativeMethods這個(gè)函數(shù)中注冊(cè)。這個(gè)函數(shù)是基于JNI_OnLoad的命名匹配方式的重點(diǎn)。
在JNI中,代碼編寫(xiě)者通過(guò)函數(shù)signature名和映射表的配合,來(lái)告訴JNI_OnLoad,你要找的函數(shù)在Native代碼中是如何定義的(signature),以及在哪定義的(映射表)。關(guān)于signature的生成和含義,在上面已經(jīng)介紹。而映射表,是Android使用的一種用于映射Java和C/C++函數(shù)的數(shù)組,這個(gè)數(shù)組的類(lèi)型是JNINativeMethod,定義為:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
其中,第一個(gè)變量是Java代碼中的函數(shù)名稱(chēng)。第二個(gè)變量是該函數(shù)對(duì)應(yīng)的Native signature。第三個(gè)變量是該函數(shù)對(duì)應(yīng)的Native函數(shù)的函數(shù)指針。例如,在上面register_Test的函數(shù)實(shí)現(xiàn)中,傳給registerNativeMethods的參數(shù)TestMethods就是映射表,定義如下:
static JNINativeMethod TestMethods[] = {
{“getPrintStr”, “()Ljava/lang/String”, (void*)test_getPrintStr}
};
其中g(shù)etPrintStr是在Java代碼中定義的函數(shù)的名稱(chēng),()Ljava/lang/String是簽名,因?yàn)樵摵瘮?shù)無(wú)參數(shù)傳入,并返回一個(gè)String。test_getPrintStr則是我們即將在Native code中定義的函數(shù)名稱(chēng)。該映射表和前面定義的類(lèi)名ClassPathName一起傳入registerNativeMethods:
static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* Methods, int numMethods) {
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
LOGE(“Native registration unable to find class ‘%s’”, className);
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
LOGE(“RegisterNatives failed for ‘%s’”, className);
return JNI_FALSE;
}
return JNI_TRUE;
}
在這里,先load目標(biāo)類(lèi),然后注冊(cè)Native函數(shù),然后返回狀態(tài)。
可以看出,通過(guò)映射表方式,Java code中的函數(shù)名不須再和Native code中的函數(shù)名呆板對(duì)應(yīng)。只需要將函數(shù)注冊(cè)進(jìn)映射表中,Native code的函數(shù)編寫(xiě)就有了很大的靈活性。雖說(shuō)和前一種傳統(tǒng)的匹配方法比,這種方式并沒(méi)有效率上的改進(jìn),因?yàn)閮烧弑举|(zhì)上都是從JNI load開(kāi)始做函數(shù)映射。但是這一種register的方法極大降低了兩邊的耦合性,所以實(shí)際使用中會(huì)受歡迎得多。比如說(shuō),由于映射表是一個(gè)<名稱(chēng),函數(shù)指針>對(duì)照表,在程序執(zhí)行時(shí),可多次調(diào)用registerNativeMethods()函數(shù)來(lái)更換本地函數(shù)指針,而達(dá)到彈性抽換本地函數(shù)的目的。
4. 接下來(lái)本應(yīng)介紹test_getPrintStr。但在此之前,簡(jiǎn)單介紹Android.mk,也就是編譯NDK所需要的Makefile,從而完成JNI信息鏈的講解。Android.mk可以基于模版修改,里面重要的變量包括:
LOCAL_C_INCLUDES:包含的頭文件。這里需要包含JNI的頭文件。
LOCAL_SRC_FILES: 包含的源文件。
LOCAL_MODULE:當(dāng)前模塊的名稱(chēng),也就是第一步中我們提到的LIBNAME。注意這個(gè)需要加上lib前綴,但不需要加.so后綴,也就是說(shuō)應(yīng)該是libLIBNAME。
LOCAL_SHARED_LIBRARIES:當(dāng)前模塊需要依賴(lài)的共享庫(kù)。
LOCAL_PRELINK_MODULE:該模塊是否被啟動(dòng)就加載。該項(xiàng)設(shè)置依具體程序的特性而定。
5. 至此,JNI作為橋梁所需要的所有信息均已就緒。JNI知道在調(diào)用Java代碼中的getPrintStr函數(shù)時(shí),需要執(zhí)行Native代碼。于是通過(guò)System.loadLibrary所加載的libLIBNAME.so找到OnLoad入口。在OnLoad中,JNI發(fā)現(xiàn)了函數(shù)映射表,發(fā)現(xiàn)getPrintStr對(duì)應(yīng)的Native函數(shù)是test_getPrintStr。于是JNI將參數(shù)(如果有的話)傳遞給test_getPrintStr并執(zhí)行,再將返回值(如果有的話)傳回Java中的getPrintStr。
6. 用于最后測(cè)試的test_getPrintStr函數(shù)實(shí)現(xiàn)如下:
const jstring testStr = env->NewStringUTF(“hello, world”);
return testStr;
然后在Java代碼中打印出返回的字符串即可。這個(gè)網(wǎng)頁(yè)詳細(xì)介紹了env可以調(diào)用的所有方法。
7. 關(guān)于測(cè)試時(shí)使用Log。調(diào)用JNI進(jìn)行Native Code的開(kāi)發(fā)有兩種環(huán)境,完整源碼環(huán)境以及NDK。兩種環(huán)境對(duì)應(yīng)的Log輸出方式也并不相同,差異則主要體現(xiàn)在需要包含的頭文件中。如果是在完整源碼編譯環(huán)境下,只要include <utils/Log.h>頭文件(位于Android-src/system/core/include/cutils),就可以使用對(duì)應(yīng)的LOGI、LOGD等方法了,當(dāng)然LOG_TAG,LOG_NDEBUG等宏值需要自定義。如果是在NDK環(huán)境下編譯,則需要include <android/log.h>頭文件(位于ndk/android-ndk-r4/platforms/android-8/arch-arm/usr/include/android/),另外自己定義宏映射,例如:
#include <android/log.h>
#ifndef LOG_TAG
#define LOG_TAG “MY_LOG_TAG”
#endif
#define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGI(…) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGW(…) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define LOGF(…) __android_log_print(ANDROID_LOG_FATAL,LOG_TAG,__VA_ARGS__)
另外,在Android.mk文件中對(duì)類(lèi)庫(kù)的應(yīng)用在兩種環(huán)境下也不相同。如果是NDK環(huán)境下,需要包括
LOCAL_LDLIBS := -llog
而在完整源碼環(huán)境下,則需要包括
LOCAL_SHARED_LIBRARIES := libutils libcutils
8. 如果希望知道如何在Native中訪問(wèn)Java類(lèi)的私有域和方法,請(qǐng)參考這篇文章
本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)
點(diǎn)擊舉報(bào)。