Native開發與JNI機制詳解

liuzxgeek發表於2014-01-04



                                                     本文來自http://blog.csdn.net/liuxian13183/ ,引用必須註明出處!



文/李森


博主導讀:博主不是程式碼大師,研究什麼都很深,Java除外,所以無論什麼問題,方案可以提供,但具體怎樣做需要鑽研,由於博主時間有限,在部落格上公開的大多是基礎性的文章,但這並不阻擋你可以豐富基礎後,走的更遠!借鑑為最近博主的主要走向,為大家提供優質的解決方案。關於Native和Jni,博主搞過一段時間,但時間有限,今天僅轉載一篇,以饗讀者!總結:為什麼要用Native?如科大訊飛,他們的輸入法實現是用C寫的,安全性很好,效能也更好,功能性強,那麼怎樣把C的功能用到Java上呢?很好的問題!Android上提供Native來使用,反之C也可以呼叫Java,相反的方式罷了,表現形式:
so包,載入包,定義呼叫方法即可。Jni呢 ?Java Native Interface,很明白,它就是這個介面。以後有機會再跟大家一起探討AIDL,跨程式通訊的問題


話休煩絮,轉入正題:


眾所周知,OPhone平臺上的應用開發主要基於Java語言,但平臺完全支援且提供了一定的Native開發能力(主要是C/C++),使得開發者可以藉助JNI更深入的實現創意。本文主要介紹OPhone平臺的JNI機制和Native模組開發與釋出的方法。

JNI簡介

Java Native Interface(JNI)是Java提供的一個很重要的特性。它使得用諸如C/C++等語言編寫的程式碼可以與執行於Java虛擬機器(JVM)中的Java程式碼整合。有些時候,Java並不能滿足你的全部開發需求,比如你希望提高某些關鍵模組的效率,或者你必須使用某個以C/C++等Native語言編寫的程式庫;此時,JNI就能滿足你在Java程式碼中訪問這些Native模組的需求。JNI的出現使得開發者既可以利用Java語言跨平臺、類庫豐富、開發便捷等特點,又可以利用Native語言的高效。

實際上,JNI是JVM實現中的一部分,因此Native語言和Java程式碼都執行在JVM的宿主環境(Host Environment),正如圖1所示。此外,JNI是一個雙向的介面:開發者不僅可以通過JNI在Java程式碼中訪問Native模組,還可以在Native程式碼中嵌入一個JVM,並通過JNI訪問執行於其中的Java模組。可見,JNI擔任了一個橋樑的角色,它將JVM與Native模組聯絡起來,從而實現了Java程式碼與Native程式碼的互訪。在OPhone上使用Java虛擬機器是為嵌入式裝置特別優化的Dalvik虛擬機器。每啟動一個應用,系統會建立一個新的程式執行一個Dalvik虛擬機器,因此各應用實際上是執行在各自的VM中的。Dalvik
VM對JNI的規範支援的較全面,對於從JDK 1.2到JDK 1.6補充的增強功能也基本都能支援。

開發者在使用JNI之前需要充分了解其優缺點,以便合理選擇技術方案實現目標。JNI的優點前面已經講過,這裡不再重複,其缺點也是顯而易見的:由於Native模組的使用,Java程式碼會喪失其原有的跨平臺性和型別安全等特性。此外,在JNI應用中,Java程式碼與Native程式碼執行於同一個程式空間內;對於跨程式甚至跨宿主環境的Java與Native間通訊的需求,可以考慮採用socket、Web Service等IPC通訊機制來實現。

在OPhone開發中使用JNI

正如我們在上一節所述,JNI是一個雙向的介面,所以互動的型別可以分為在Java程式碼中呼叫Native模組和在Native程式碼中呼叫Java模組兩種。下面,我們就使用一個Hello-JNI的示例來分別對這兩種互動方式的開發要點加以說明。

Java呼叫Native模組

Hello-JNI這個示例的結構很簡單:首先我們使用Eclipse新建一個OPhone應用的Java工程,並新增一個com.example.hellojni.HelloJni的類。這個類實際上是一個Activity,稍後我們會建立一個TextView,並顯示一些文字在上面。

要在Java程式碼中使用Native模組,必須先對Native函式進行宣告。在我們的例子中,開啟HelloJni.java檔案,可以看到如下的宣告:
眾所周知,OPhone平臺上的應用開發主要基於Java語言,但平臺完全支援且提供了一定的Native開發能力(主要是C/C++),使得開發者可以藉助JNI更深入的實現創意。本文主要介紹OPhone平臺的JNI機制和Native模組開發與釋出的方法。


  1. /* A native method that is implemented by the
  2. * `hello-jni` native library, which is packaged
  3. * with this application.
  4. */
  5. public native String stringFromJNI();

從上述宣告中我們可以知道,這個stringFromJNI()函式就是要在Java程式碼中呼叫的Native函式。接下來我們要建立一個hello-jni.c的C檔案,內容很簡單,只有如下一個函式:


  1. #include <string.h>
  2. #include <jni.h>
  3. jstring
  4. Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
  5. jobject thiz ) {
  6. return (*env)->NewStringUTF(env, “Hello from JNI !”);
  7. }

從函式名可以看出,這個Native函式對應的正是我們在com.example.hellojni.HelloJni這個中宣告的Native函式String stringFromJNI()的具體實現。

從上面Native函式的命名上我們可以瞭解到JNI函式的命名規則: Java程式碼中的函式宣告需要新增native關鍵字;Native的對應函式名要以“Java_”開頭,後面依次跟上Java的“package名”、“class名”、“函式名”,中間以下劃線“_”分割,在package名中的“.”也要改為“_”。此外,關於函式的引數和返回值也有相應的規則。對於Java中的基本型別如int、double、char等,在Native端都有相對應的型別來表示,如jint、jdouble、jchar等;其他的物件型別則統統由jobject來表示(String是個例外,由於其使用廣泛,故在Native程式碼中有jstring這個型別來表示,正如在上例中返回值String對應到Native程式碼中的返回值jstring)。而對於Java中的陣列,在Native中由jarray對應,具體到基本型別和一般物件型別的陣列則有jintArray等和jobjectArray分別對應(String陣列在這裡沒有例外,同樣用jobjectArray表示)。還有一點需要注意的是,在JNI的Native函式中,其前兩個引數JNIEnv*和jobject是必需的——前者是一個JNIEnv結構體的指標,這個結構體中定義了很多JNI的介面函式指標,使開發者可以使用JNI所定義的介面功能;後者指代的是呼叫這個JNI函式的Java物件,有點類似於C++中的this指標。在上述兩個引數之後,還需要根據Java端的函式宣告依次對應新增引數。在上例中,Java中宣告的JNI函式沒有引數,則Native的對應函式只有型別為JNIEnv*和jobject的兩個引數。

當然,要使用JNI函式,還需要先載入Native程式碼編譯出來的動態庫檔案(在Windows上是.dll,在Linux上則為.so)。這個動作是通過如下語句完成的:


  1. static {
  2. System.loadLibrary(“hello-jni”);
  3. }

注意這裡呼叫的共享庫名遵循Linux對庫檔案的命名慣例,因為OPhone的核心實際上是Linux系統——上例中,實際載入的庫檔案應為“libhello-jni.so”,在引用時遵循命名慣例,不帶“lib”字首和“.so”的副檔名。對於沒有按照上述慣例命名的Native庫,在載入時仍需要寫成完整的檔名。

JNI函式的使用方法和普通Java函式一樣。在本例中,呼叫程式碼如下:


  1. TextView tv = new TextView(this);
  2. tv.setText( stringFromJNI() );
  3. setContentView(tv);

就可以在TextView中顯示出來自於Native函式的字串。怎麼樣,是不是很簡單呢?

Native呼叫Java模組

從OPhone的系統架構來看,JVM和Native系統庫位於核心之上,構成OPhone Runtime;更多的系統功能則是通過在其上的Application Framework以Java API的形式提供的。因此,如果希望在Native庫中呼叫某些系統功能,就需要通過JNI來訪問Application Framework提供的API。
眾所周知,OPhone平臺上的應用開發主要基於Java語言,但平臺完全支援且提供了一定的Native開發能力(主要是C/C++),使得開發者可以藉助JNI更深入的實現創意。本文主要介紹OPhone平臺的JNI機制和Native模組開發與釋出的方法。

JNI規範定義了一系列在Native程式碼中訪問Java物件及其成員與方法的API。下面我們還是通過示例來具體講解。首先,新建一個SayHello的類,程式碼如下:


  1. package com.example.hellojni;
  2. public class SayHello {
  3. public String sayHelloFromJava(String nativeMsg) {
  4. String str = nativeMsg + ” But shown in Java!”;
  5. return str;
  6. }
  7. }

接下來要實現的就是在Native程式碼中呼叫這個SayHello類中的sayHelloFromJava方法。

一般來說,要在Native程式碼中訪問Java物件,有如下幾個步驟:

1. 得到該Java物件的類定義。JNI定義了jclass這個型別來表示Java的類的定義,並提供了FindClass介面,根據類的完整的包路徑即可得到其jclass。

2. 根據jclass建立相應的物件實體,即jobject。在Java中,建立一個新物件只需要使用new關鍵字即可,但在Native程式碼中建立一個物件則需要兩步:首先通過JNI介面GetMethodID得到該類的建構函式,然後利用NewObject介面構造出該類的一個例項物件。

3. 訪問jobject中的成員變數或方法。訪問物件的方法是先得到方法的Method ID,然後使用CallMethod介面呼叫,這裡Type對應相應方法的返回值——返回值為基本型別的都有相對應的介面,如CallIntMethod;其他的返回值(包括String)則為CallObjectMethod。可以看出,建立物件實質上是呼叫物件的一個特殊方法,即建構函式。訪問成員變數的步驟一樣:首先GetFieldID得到成員變數的ID,然後Get/SetField讀/寫變數值。

上面概要介紹了從Native程式碼中訪問Java物件的過程,下面我們結合示例來具體看一下。如下是呼叫sayHelloFromJava方法的Native程式碼:


  1. jstring helloFromJava( JNIEnv* env ) {
  2. jstring str = NULL;
  3. jclass clz = (*env)->FindClass(env, “com/example/hellojni/SayHello”);
  4. jmethodID ctor = (*env)->GetMethodID(env, clz, “<init>“, “()V”);
  5. jobject obj = (*env)->NewObject(env, clz, ctor);
  6. jmethodID mid = (*env)->GetMethodID(env, clz, “sayHelloFromJava”, “(Ljava/lang/String;)Ljava/lang/String;”);
  7. if (mid) {
  8. jstring jmsg = (*env)->NewStringUTF(env, “I`m born in native.”);
  9. str = (*env)->CallObjectMethod(env, obj, mid, jmsg);
  10. }
  11. return str;
  12. }

可以看到,上述程式碼和前面講到的步驟完全相符。這裡提一下程式設計時要注意的要點:1、FindClass要寫明Java類的完整包路徑,並將“.”以“/”替換;2、GetMethodID的第三個引數是方法名(對於建構函式一律用“”表示),第四個引數是方法的“簽名”,需要用一個字串序列表示方法的引數(依宣告順序)和返回值資訊。由於篇幅所限,這裡不再具體說明如何根據方法的宣告構造相應的“簽名”,請參考JNI的相關文件。

關於上面談到的步驟再補充說明一下:在JNI規範中,如上這種使用NewObject建立的物件例項被稱為“Local Reference”,它僅在建立它的Native程式碼作用域內有效,因此應避免在作用域外使用該例項及任何指向它的指標。如果希望建立的物件例項在作用域外也能使用,則需要使用NewGlobalRef介面將其提升為“Global Reference”——需要注意的是,當Global Reference不再使用後,需要顯式的釋放,以便通知JVM進行垃圾收集。

Native模組的編譯與釋出

通過前面的介紹,我們已經大致瞭解了在OPhone的應用開發中使用JNI的方法。那麼,開發者如何編譯出能在OPhone上使用的Native模組呢?編譯出的Native模組又如何像APK檔案那樣分發、安裝呢?

Google於2009年6月底釋出了Android NDK的第一個版本,為廣大開發者提供了編譯用於Android應用的Native模組的能力,以及將Native模組隨Java應用打包為APK檔案,以便分發和安裝的整套解決方案。NDK的全稱是Native Development Toolkit,即原生應用開發包。由於OPhone平臺也基於Android,因此使用Android NDK編譯的原生應用或元件完全可以用於OPhone。需要注意的是,Google聲稱此次釋出的NDK僅相容於Android
1.5及以後的版本,由於OPhone 1.0平臺基於Android 1.5之前的版本,雖然不排除使用該NDK開發的原生應用或元件在OPhone 1.0平臺上正常執行的可能性,但建議開發者僅在OPhone 1.5及以上的平臺使用。
眾所周知,OPhone平臺上的應用開發主要基於Java語言,但平臺完全支援且提供了一定的Native開發能力(主要是C/C++),使得開發者可以藉助JNI更深入的實現創意。本文主要介紹OPhone平臺的JNI機制和Native模組開發與釋出的方法。

最新版本的NDK可以在http://developer.android.com/sdk/ndk/index.html下載。NDK提供了適用於Windows、Linux和MAC OS X的版本,開發者可以根據自己的作業系統下載相應的版本。本文僅使用基於Linux的NDK版本做介紹和演示。

NDK的安裝很簡單:解壓到某個路徑下即可,之後可以看到若干目錄。其中docs目錄中包含了比較詳細的文件,可供開發者參考,在NDK根目錄下的README.TXT也對個別重要文件進行了介紹;build目錄則包含了用於Android裝置的交叉編譯器和相關工具,以及一組系統標頭檔案和系統庫,其中包括libc、libm、libz、liblog(用於Android裝置log輸出)、JNI介面及一個C++標準庫的子集(所謂“子集”是指Android對C++支援有限,如不支援Exception及STL等);apps目錄是用於應用開發的目錄,out目錄則用於編譯中間結果的儲存。接下來,我們就用前面的例子簡單講解一下NDK的使用。

進入/apps目錄,我們可以看到一些示例應用,以hello-jni為例:在hello-jni目錄中有一個Application.mk檔案和一個project資料夾,project資料夾中則是一個OPhone Java應用所有的工程檔案,其中jni目錄就是Native程式碼放置的位置。這裡Application.mk主要用於告訴編譯器應用所需要用到的Native模組有什麼,對於一般開發在示例提供的檔案的基礎上進行修改即可;如果需要了解更多,可參考/docs/APPLICATION-MK.txt。接下來,我們將示例檔案與程式碼如圖2放置到相應的位置:

可以看到,和Java應用一樣,Native模組也需要使用Android.mk檔案設定編譯選項和引數,但內容有較大不同。對於Native模組而言,一般需要了解如下幾類標籤:

1.LOCAL_MODULE:定義了在整個編譯環境中的各個模組,其名字應當是唯一的。此外,這裡設定的模組名稱還將作為編譯出來的檔名:對於原生可執行檔案,檔名即為模組名稱;對於靜態/動態庫檔案,檔名為lib+模組名稱。例如hello-jni的模組名稱為“hello-jni”,則編譯出來的動態庫就是libhello-jni.so。

2.LOCAL_SRC_FILES:這裡要列出所有需要編譯的C/C++原始檔,以空格或製表符分隔;如需換行,可放置“”符號在行尾,這和GNU Makefile的規則是一致的。

3.LOCAL_CFLAGS:定義gcc編譯時的CFLAGS引數,與GNU Makefile的規則一致。比如,用-I引數可指定編譯所需引用的某個路徑下的標頭檔案。

4.LOCAL_C_INCLUDES:指定自定義的標頭檔案路徑。

5.LOCAL_SHARED_LIBRARIES:定義連結時所需要的共享庫檔案。這裡要連結的共享庫並不限於NDK編譯環境中定義的所有模組。如果需要引用其他的庫檔案,也可在此處指定。

6.LOCAL_STATIC_LIBRARIES:和上個標籤類似,指定需要連結的靜態庫檔案。需要注意的是這個選項只有在編譯動態庫的時候才有意義。

7.LOCAL_LDLIBS:定義連結時需要引入的系統庫。使用時需要加-l字首,例如-lz指的是在載入時連結libz這個系統庫。libc、libm和libstdc++是編譯系統預設會連結的,無需在此標籤中指定。

欲瞭解更多關於標籤型別及各類標籤的資訊,可參考/docs/ANDROID-MK.txt檔案,其中詳細描述了Android.mk中各個標籤的含義與用法。如下給出的就是我們的示例所用的Android.mk:


  1. LOCAL_PATH := $(call my-dir)
  2. include $(CLEAR_VARS)
  3. LOCAL_MODULE := hello-jni
  4. LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
  5. LOCAL_SRC_FILES := src/call_java.c
  6. src/hello-jni.c
  7. include $(BUILD_SHARED_LIBRARY)

寫好了程式碼和Makefile,接下來就是編譯了。使用NDK進行編譯也很簡單:首先從命令列進入目錄,執行./build/host-setup.sh,當列印出“Host setup complete.”的文字時,編譯環境的設定就完成了。這裡開發者需要注意的是,如果使用的Linux發行版是Debian或者Ubuntu,需要通過在目錄下執行bash build/host-setup.sh,因為上述兩個發行版使用的dash shell與指令碼有相容問題。接下來,輸入make
APP=hello-jni,稍等片刻即完成編譯,如圖3所示。從圖中可以看到,在編譯完成後,NDK會自動將編譯出來的共享庫拷貝到Java工程的libs/armeabi目錄下。當編譯Java工程的時候,相應的共享庫會被一同打包到apk檔案中。在應用安裝時,被打包在libs/armeabi目錄中的共享庫會被自動拷貝到/data/data/com.example.HelloJni/lib/目錄;當System.loadLibrary被呼叫時,系統就可以在上述目錄尋找到所需的庫檔案libhello-jni.so。如果實際的Java工程不在這裡,也可以手動在Java工程下建立libs/armeabi目錄,並將編譯出來的so庫檔案拷貝過去。

最後,將Java工程連帶庫檔案一同編譯並在OPhone模擬器中執行,結果如圖4所示。

通過上面的介紹,你應該已經對OPhone上的Native開發有了初步瞭解,或許也已經躍躍欲試了。事實上,儘管Native開發在OPhone上不具有Java語言的型別安全、相容性好、易於除錯等特性,也無法直接享受平臺提供的豐富的API,但JNI還是為我們提供了更多的選擇,使我們可以利用原生應用的優勢來做對效能要求高的操作,也可以利用或移植C/C++領域現有的眾多功能強大的類庫或應用,為開發者提供了充分的施展空間。這就是OPhone的魅力!

喜歡的就點個贊吧!

轉自:http://developer.51cto.com/art/201007/213701_3.htm



相關文章