OPhone平臺(tái)Native開發(fā)與JNI機(jī)制詳解
JNI簡(jiǎn)介
Java Native Interface(JNI)是Java提供的一個(gè)很重要的特性。它使得用諸如C/C++等語言編寫的代碼可以與運(yùn)行于Java虛擬機(jī)(JVM)中的Java代碼集成。有些時(shí)候,Java并不能滿足你的全部開發(fā)需求,比如你希望提高某些關(guān)鍵模塊的效率,或者你必須使用某個(gè)以C/C++等Native語言編寫的程序庫;此時(shí),JNI就能滿足你在Java代碼中訪問這些Native模塊的需求。JNI的出現(xiàn)使得開發(fā)者既可以利用Java語言跨平臺(tái)、類庫豐富、開發(fā)便捷等特點(diǎn),又可以利用Native語言的高效。實(shí)際上,JNI是JVM實(shí)現(xiàn)中的一部分,因此Native語言和Java代碼都運(yùn)行在JVM的宿主環(huán)境(Host Environment),正如圖1所示。此外,JNI是一個(gè)雙向的接口:開發(fā)者不僅可以通過JNI在Java代碼中訪問Native模塊,還可以在Native代碼中嵌入一個(gè)JVM,并通過JNI訪問運(yùn)行于其中的Java模塊??梢?,JNI擔(dān)任了一個(gè)橋梁的角色,它將JVM與Native模塊聯(lián)系起來,從而實(shí)現(xiàn)了Java代碼與Native代碼的互訪。在OPhone上使用Java虛擬機(jī)是為嵌入式設(shè)備特別優(yōu)化的Dalvik虛擬機(jī)。每啟動(dòng)一個(gè)應(yīng)用,系統(tǒng)會(huì)建立一個(gè)新的進(jìn)程運(yùn)行一個(gè)Dalvik虛擬機(jī),因此各應(yīng)用實(shí)際上是運(yùn)行在各自的VM中的。Dalvik VM對(duì)JNI的規(guī)范支持的較全面,對(duì)于從JDK 1.2到JDK 1.6補(bǔ)充的增強(qiáng)功能也基本都能支持。
開發(fā)者在使用JNI之前需要充分了解其優(yōu)缺點(diǎn),以便合理選擇技術(shù)方案實(shí)現(xiàn)目標(biāo)。JNI的優(yōu)點(diǎn)前面已經(jīng)講過,這里不再重復(fù),其缺點(diǎn)也是顯而易見的:由于Native模塊的使用,Java代碼會(huì)喪失其原有的跨平臺(tái)性和類型安全等特性。此外,在JNI應(yīng)用中,Java代碼與Native代碼運(yùn)行于同一個(gè)進(jìn)程空間內(nèi);對(duì)于跨進(jìn)程甚至跨宿主環(huán)境的Java與Native間通信的需求,可以考慮采用socket、Web Service等IPC通信機(jī)制來實(shí)現(xiàn)。 在OPhone開發(fā)中使用JNI 正如我們?cè)谏弦还?jié)所述,JNI是一個(gè)雙向的接口,所以交互的類型可以分為在Java代碼中調(diào)用Native模塊和在Native代碼中調(diào)用Java模塊兩種。下面,我們就使用一個(gè)Hello-JNI的示例來分別對(duì)這兩種交互方式的開發(fā)要點(diǎn)加以說明。 Java調(diào)用Native模塊 Hello-JNI這個(gè)示例的結(jié)構(gòu)很簡(jiǎn)單:首先我們使用Eclipse新建一個(gè)OPhone應(yīng)用的Java工程,并添加一個(gè)com.example.hellojni.HelloJni的類。這個(gè)類實(shí)際上是一個(gè)Activity,稍后我們會(huì)創(chuàng)建一個(gè)TextView,并顯示一些文字在上面。 要在Java代碼中使用Native模塊,必須先對(duì)Native函數(shù)進(jìn)行聲明。在我們的例子中,打開HelloJni.java文件,可以看到如下的聲明:#p#從上述聲明中我們可以知道,這個(gè)stringFromJNI()函數(shù)就是要在Java代碼中調(diào)用的Native函數(shù)。接下來我們要?jiǎng)?chuàng)建一個(gè)hello-jni.c的C文件,內(nèi)容很簡(jiǎn)單,只有如下一個(gè)函數(shù):
- /* A native method that is implemented by the
- * 'hello-jni' native library, which is packaged
- * with this application.
- */
- public native String stringFromJNI();
從函數(shù)名可以看出,這個(gè)Native函數(shù)對(duì)應(yīng)的正是我們?cè)赾om.example.hellojni.HelloJni這個(gè)中聲明的Native函數(shù)String stringFromJNI()的具體實(shí)現(xiàn)。 從上面Native函數(shù)的命名上我們可以了解到JNI函數(shù)的命名規(guī)則: Java代碼中的函數(shù)聲明需要添加native關(guān)鍵字;Native的對(duì)應(yīng)函數(shù)名要以“Java_”開頭,后面依次跟上Java的“package名”、“class名”、“函數(shù)名”,中間以下劃線“_”分割,在package名中的“.”也要改為“_”。此外,關(guān)于函數(shù)的參數(shù)和返回值也有相應(yīng)的規(guī)則。對(duì)于Java中的基本類型如int、double、char等,在Native端都有相對(duì)應(yīng)的類型來表示,如jint、jdouble、jchar等;其他的對(duì)象類型則統(tǒng)統(tǒng)由jobject來表示(String是個(gè)例外,由于其使用廣泛,故在Native代碼中有jstring這個(gè)類型來表示,正如在上例中返回值String對(duì)應(yīng)到Native代碼中的返回值jstring)。而對(duì)于Java中的數(shù)組,在Native中由jarray對(duì)應(yīng),具體到基本類型和一般對(duì)象類型的數(shù)組則有jintArray等和jobjectArray分別對(duì)應(yīng)(String數(shù)組在這里沒有例外,同樣用jobjectArray表示)。還有一點(diǎn)需要注意的是,在JNI的Native函數(shù)中,其前兩個(gè)參數(shù)JNIEnv*和jobject是必需的——前者是一個(gè)JNIEnv結(jié)構(gòu)體的指針,這個(gè)結(jié)構(gòu)體中定義了很多JNI的接口函數(shù)指針,使開發(fā)者可以使用JNI所定義的接口功能;后者指代的是調(diào)用這個(gè)JNI函數(shù)的Java對(duì)象,有點(diǎn)類似于C++中的this指針。在上述兩個(gè)參數(shù)之后,還需要根據(jù)Java端的函數(shù)聲明依次對(duì)應(yīng)添加參數(shù)。在上例中,Java中聲明的JNI函數(shù)沒有參數(shù),則Native的對(duì)應(yīng)函數(shù)只有類型為JNIEnv*和jobject的兩個(gè)參數(shù)。 當(dāng)然,要使用JNI函數(shù),還需要先加載Native代碼編譯出來的動(dòng)態(tài)庫文件(在Windows上是.dll,在Linux上則為.so)。這個(gè)動(dòng)作是通過如下語句完成的:
- #include <string.h>
- #include <jni.h>
- jstring
- Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
- jobject thiz ) {
- return (*env)->NewStringUTF(env, "Hello from JNI !");
- }
注意這里調(diào)用的共享庫名遵循Linux對(duì)庫文件的命名慣例,因?yàn)镺Phone的核心實(shí)際上是Linux系統(tǒng)——上例中,實(shí)際加載的庫文件應(yīng)為“l(fā)ibhello-jni.so”,在引用時(shí)遵循命名慣例,不帶“l(fā)ib”前綴和“.so”的擴(kuò)展名。對(duì)于沒有按照上述慣例命名的Native庫,在加載時(shí)仍需要寫成完整的文件名。 JNI函數(shù)的使用方法和普通Java函數(shù)一樣。在本例中,調(diào)用代碼如下:
- static {
- System.loadLibrary("hello-jni");
- }
就可以在TextView中顯示出來自于Native函數(shù)的字符串。怎么樣,是不是很簡(jiǎn)單呢? Native調(diào)用Java模塊 從OPhone的系統(tǒng)架構(gòu)來看,JVM和Native系統(tǒng)庫位于內(nèi)核之上,構(gòu)成OPhone Runtime;更多的系統(tǒng)功能則是通過在其上的Application Framework以Java API的形式提供的。因此,如果希望在Native庫中調(diào)用某些系統(tǒng)功能,就需要通過JNI來訪問Application Framework提供的API。#p# JNI規(guī)范定義了一系列在Native代碼中訪問Java對(duì)象及其成員與方法的API。下面我們還是通過示例來具體講解。首先,新建一個(gè)SayHello的類,代碼如下:
- TextView tv = new TextView(this);
- tv.setText( stringFromJNI() );
- setContentView(tv);
接下來要實(shí)現(xiàn)的就是在Native代碼中調(diào)用這個(gè)SayHello類中的sayHelloFromJava方法。 一般來說,要在Native代碼中訪問Java對(duì)象,有如下幾個(gè)步驟: 1. 得到該Java對(duì)象的類定義。JNI定義了jclass這個(gè)類型來表示Java的類的定義,并提供了FindClass接口,根據(jù)類的完整的包路徑即可得到其jclass。 2. 根據(jù)jclass創(chuàng)建相應(yīng)的對(duì)象實(shí)體,即jobject。在Java中,創(chuàng)建一個(gè)新對(duì)象只需要使用new關(guān)鍵字即可,但在Native代碼中創(chuàng)建一個(gè)對(duì)象則需要兩步:首先通過JNI接口GetMethodID得到該類的構(gòu)造函數(shù),然后利用NewObject接口構(gòu)造出該類的一個(gè)實(shí)例對(duì)象。 3. 訪問jobject中的成員變量或方法。訪問對(duì)象的方法是先得到方法的Method ID,然后使用Call
- package com.example.hellojni;
- public class SayHello {
- public String sayHelloFromJava(String nativeMsg) {
- String str = nativeMsg + " But shown in Java!";
- return str;
- }
- }
可以看到,上述代碼和前面講到的步驟完全相符。這里提一下編程時(shí)要注意的要點(diǎn):1、FindClass要寫明Java類的完整包路徑,并將“.”以“/”替換;2、GetMethodID的第三個(gè)參數(shù)是方法名(對(duì)于構(gòu)造函數(shù)一律用“
- jstring helloFromJava( JNIEnv* env ) {
- jstring str = NULL;
- jclass clz = (*env)->FindClass(env, "com/example/hellojni/SayHello");
- jmethodID ctor = (*env)->GetMethodID(env, clz, "<init>", "()V");
- jobject obj = (*env)->NewObject(env, clz, ctor);
- jmethodID mid = (*env)->GetMethodID(env, clz, "sayHelloFromJava", "(Ljava/lang/String;)Ljava/lang/String;");
- if (mid) {
- jstring jmsg = (*env)->NewStringUTF(env, "I'm born in native.");
- str = (*env)->CallObjectMethod(env, obj, mid, jmsg);
- }
- return str;
- }
- LOCAL_PATH := $(call my-dir)
- include $(CLEAR_VARS)
- LOCAL_MODULE := hello-jni
- LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
- LOCAL_SRC_FILES := src/call_java.c \
- src/hello-jni.c
- include $(BUILD_SHARED_LIBRARY)
寫好了代碼和Makefile,接下來就是編譯了。使用NDK進(jìn)行編譯也很簡(jiǎn)單:首先從命令行進(jìn)入
【編輯推薦】