下面我就用JNI實作一個經典的“Hello World”程式。該程式在Java中通過JNI調用c函數實作“Hello World”的輸出。建立該程式分為以下步驟:
1、建立一個Java程式(HelloWorld.java)定義原生的c/c++函數。
2、用javac編譯HelloWorld.java生成HelloWorld.class。
3、用javah帶-jni參數編譯HelloWorld.class生成HelloWorld.h檔案,該檔案中定義了c的函數原型。在實作c函數的時候需要。
4、建立HelloWorld.c,實作HelloWorld.h定義的函數。
5、編譯HelloWorld.c生成libHelloWorld.so。
6、在java虛拟機運作java程式HelloWorld。
下面我們就一步一步來實作這個程式。
注意print方法的聲明,關鍵字native表明該方法是一個原生代碼實作的。另外注意static代碼段的System.loadLibrary調用,這段代碼表示在程式加載的時候,自動加載libHelloWorld.so庫。
在指令行中運作如下指令:
在目前檔案夾編譯生成HelloWorld.class。
在目前檔案夾中會生成HelloWorld.h。打開HelloWorld.h将會發現如下代碼:
該檔案中包含了一個函數Java_HelloWorld_print的聲明。這裡面包含兩個參數,非常重要,後面講實作的時候會講到。
建立HelloWorld.c檔案輸入如下的代碼:
注意必須要包含jni.h頭檔案,該檔案中定義了JNI用到的各種類型,宏定義等。
另外需要注意Java_HelloWorld_print的兩個參數,本例比較簡單,不需要用到這兩個參數。但是這兩個參數在JNI中非常重要。
env代表java虛拟機環境,Java傳過來的參數和c有很大的不同,需要調用JVM提供的接口來轉換成C類型的,就是通過調用env方法來完成轉換的。
obj代表調用的對象,相當于c++的this。當c函數需要改變調用對象成員變量時,可以通過操作這個對象來完成。
在Linux下執行如下指令來完成編譯工作:
(注:合理設定$PATH可以簡化編譯指令 如gcc -shared -fPIC hello_jni.c -o libHelloWorld.so)
在目前目錄生成libHelloWorld.so。注意一定需要包含Java的include目錄(請根據自己系統環境設定),因為Helloworld.c中包含了jni.h。
另外一個值得注意的是在HelloWorld.java中我們LoadLibrary方法加載的是“HelloWorld”,可我們生成的Library卻是libHelloWorld。這是Linux的連結規定的,一個庫的必須要是:lib+庫名+.so。連結的時候隻需要提供庫名就可以了。
運作Java程式HelloWorld
大功告成最後一步,驗證前面的成果的時刻到了:
如果你這步發生問題,如果這步你收到java.lang.UnsatisfiedLinkError異常,可以通過如下方式指明共享庫的路徑:
我們可以看到久違的“Hello world!”輸出了。
sudo cp jni.h /usr/include/
(注:編譯是找不到jni.h 或者jni_md.h, 需要把%JAVA_HOME%/include/jni.h 和 %JAVA_HOME%/include/linux/jni_md.h拷貝到 /usr/include/一份)
(注:
1、 在你載入jni類之前 放入“System.out.println(System.getProperty("java.library.path"));
2、運作你的程式你将獲得java.library.path指向的目錄
3、拷貝你的libxxx.so到java.library.path指向的某個目錄下面。
一定要将linux下的共享庫(我暫且這麼叫:)命名成libxxx.so的形式,"xxx"是你在System.loadLibrary("xxx")中用到的加載庫名稱。
也可以通過設定LINUX下的系統變量LD_LIBRARY_PATH來添加java.library.path,隻要在啟動~/.bashrc中添加如下代碼然後重新登入shell,就可以将動态庫放在目前目錄下運作你的jni程式了。export LD_LIBRARY_PATH=.:..:$LD_LIBRARY_PATH)
下篇:Android NDK HelloJNI
入門的最好辦法就是學習Android自帶的例子, 這裡就通過學習Android的NDK自帶的demo程式:hello-jni來達到這個目的。
1、 開發環境的搭建
1)android的NDK開發需要在linux下進行: 因為需要把C/C++編寫的代碼生成能在arm上運作的.so檔案,這就需要用到交叉編譯環境,而交叉編譯需要在linux系統下才能完成。
2)安裝android-ndk開發包,這個開發包可以在google android 官網下載下傳: 通過這個開發包的工具才能将android jni 的C/C++的代碼編譯成庫
3)android應用程式開發環境: 包括eclipse、java、 android sdk、 adt等。
如何下載下傳和安裝android-ndk我這裡就不啰嗦了,安裝完之後,需要将android-ndk的路勁加到環境變量PATH中:
sudo gedit /etc/environment
在environment的PATH環境變量中添加你的android-ndk的安裝路勁,然後再讓這個更改的環境變量立即生效:
source /etc/environment
經過了上述步驟,在指令行下敲:
ndk-bulid
彈出如下的錯誤,而不是說ndk-build not found,就說明ndk環境已經安裝成功了。
Android NDK: Could not find application project directory !
Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
/home/braincol/workspace/android/android-ndk-r5/build/core/build-local.mk:85: *** Android NDK: Aborting . Stop.
2.代碼的編寫
1)首先是寫java代碼
建立一個Android應用工程HelloJni,建立HelloJni.java檔案:
HelloJni.java :
<a target="_blank"></a>
這段代碼很簡單,注釋也很清晰,這裡隻提兩點::
<a href="http://www.cnblogs.com/devinzhang/archive/2012/02/29/2373729.html" target="_blank">?</a>
<code>static</code><code>{</code>
<code>System.loadLibrary(</code><code>"hello-jni"</code><code>);</code>
<code>}</code>
表明程式開始運作的時候會加載hello-jni, static區聲明的代碼會先于onCreate方法執行。如果你的程式中有多個類,而且如果HelloJni這個類不是你應用程式的入口,那麼hello-jni(完整的名字是libhello-jni.so)這個庫會在第一次使用HelloJni這個類的時候加載。
<code>public</code> <code>native String stringFromJNI();</code>
<code>public</code> <code>native String unimplementedStringFromJNI();</code>
可以看到這兩個方法的聲明中有 native 關鍵字, 這個關鍵字表示這兩個方法是本地方法,也就是說這兩個方法是通過本地代碼(C/C++)實作的,在java代碼中僅僅是聲明。
用eclipse編譯該工程,生成相應的.class檔案,這步必須在下一步之前完成,因為生成.h檔案需要用到相應的.class檔案。
2)編寫相應的C/C++代碼
剛開始學的時候,有個問題會讓人很困惑,相應的C/C++代碼如何編寫,函數名如何定義? 這裡講一個方法,利用javah這個工具生成相應的.h檔案,然後根據這個.h檔案編寫相應的C/C++代碼。
a. 生成相應.h檔案:
就拿我這的環境來說,首先在終端下進入剛剛建立的HelloJni工程的目錄:
<code>braincol@ubuntu:~$ cd workspace/android/NDK/hello-jni/</code>
ls檢視工程檔案
<code>braincol@ubuntu:~/workspace/android/NDK/hello-jni$ ls</code>
<code>AndroidManifest.xml assets bin </code><code>default</code><code>.properties gen res src</code>
可以看到目前僅僅有幾個标準的android應用程式的檔案(夾)。
首先我們在工程目錄下建立一個jni檔案夾:
<code>braincol@ubuntu:~/workspace/android/NDK/hello-jni$ mkdir jni</code>
<code>AndroidManifest.xml assets bin </code><code>default</code><code>.properties gen jni res src</code>
下面就可以生成相應的.h檔案了:
<code>braincol@ubuntu:~/workspace/android/NDK/hello-jni$ javah -classpath bin -d jni com.example.hellojni.HelloJni</code>
-classpath bin:表示類的路勁
-d jni: 表示生成的頭檔案存放的目錄
com.example.hellojni.HelloJni 則是完整類名
這一步的成功要建立在已經在 bin/com/example/hellojni/ 目錄下生成了 HelloJni.class的基礎之上。現在可以看到jni目錄下多了個.h檔案:
<code>braincol@ubuntu:~/workspace/android/NDK/hello-jni$ cd jni/</code>
<code>braincol@ubuntu:~/workspace/android/NDK/hello-jni/jni$ ls</code>
<code>com_example_hellojni_HelloJni.h</code>
我們來看看com_example_hellojni_HelloJni.h的内容:
com_example_hellojni_HelloJni.h :
上面代碼中的JNIEXPORT 和 JNICALL 是jni的宏,在android的jni中不需要,當然寫上去也不會有錯。從上面的源碼中可以看出這個函數名那是相當的長啊。。。。 不過還是很有規律的, 完全按照:java_pacakege_class_mathod 形式來命名。
也就是說:
Hello.java中 stringFromJNI() 方法對應于 C/C++中的 Java_com_example_hellojni_HelloJni_stringFromJNI() 方法
HelloJni.java中的 unimplementedStringFromJNI() 方法對應于 C/C++中的 Java_com_example_hellojni_HelloJni_unimplementedStringFromJNI() 方法
注意下其中的注釋:
Signature: ()Ljava/lang/String;
()Ljava/lang/String;()表示函數的參數為空(這裡為空是指除了JNIEnv *, jobject 這兩個參數之外沒有其他參數,JNIEnv*, jobject是所有jni函數必有的兩個參數,分别表示jni環境和對應的java類(或對象)本身),Ljava/lang/String; 表示函數的傳回值是java的String對象。
b. 編寫相應的.c檔案:
hello-jni.c :
這裡隻是實作了Java_com_example_hellojni_HelloJni_stringFromJNI方法,而 Java_com_example_hellojni_HelloJni_unimplementedStringFromJNI 方法并沒有實作,因為在HelloJni.java中隻調用了stringFromJNI()方法,是以unimplementedStringFromJNI()方法沒有實作也沒關系,不過建議最好還是把所有java中定義的本地方法都實作了,寫個空函數也行啊。。。有總比沒有好。
Java_com_example_hellojni_HelloJni_stringFromJNI() 函數隻是簡單的傳回了一個内容為 "Hello from JNI !" 的jstring對象(對應于java中的String對象)。hello-jni.c檔案已經編寫好了,現在可以把com_example_hellojni_HelloJni.h檔案給删了,當然留着也行,隻是我還是習慣把不需要的檔案給清理幹淨了。
3)編譯hello-jni.c 生成相應的庫
a 編寫Android.mk檔案
在jni目錄下(即hello-jni.c 同級目錄下)建立一個Android.mk檔案,Android.mk 檔案是Android 的 makefile檔案,内容如下:
這個Androd.mk檔案很短,下面我們來逐行解釋下:
LOCAL_PATH := $(call my-dir)
一個Android.mk 檔案首先必須定義好LOCAL_PATH變量。它用于在開發樹中查找源檔案。在這個例子中,宏函數’my-dir’, 由編譯系統提供,用于傳回目前路徑(即包含Android.mk file檔案的目錄)。
include $( CLEAR_VARS)
CLEAR_VARS由編譯系統提供,指定讓GNU MAKEFILE為你清除許多LOCAL_XXX變量(例如 LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES, 等等...), 除LOCAL_PATH 。這是必要的,因為所有的編譯控制檔案都在同一個GNU MAKE執行環境中,所有的變量都是全局的。
LOCAL_MODULE := hello-jni
編譯的目标對象,LOCAL_MODULE變量必須定義,以辨別你在Android.mk檔案中描述的每個子產品。名稱必須是唯一的,而且不包含任何空格。
注意:編譯系統會自動産生合适的字首和字尾,換句話說,一個被命名為'hello-jni'的共享庫子產品,将會生成'libhello-jni.so'檔案。
重要注意事項:如果你把庫命名為‘libhello-jni’,編譯系統将不會添加任何的lib字首,也會生成 'libhello-jni.so',這是為了支援來源于Android平台的源代碼的Android.mk檔案,如果你确實需要這麼做的話。
LOCAL_SRC_FILES := hello-jni.c
LOCAL_SRC_FILES變量必須包含将要編譯打包進子產品中的C或C++源代碼檔案。注意,你不用在這裡列出頭檔案和包含檔案,因為編譯系統将會自動為你找出依賴型的檔案;僅僅列出直接傳遞給編譯器的源代碼檔案就好。
注意,預設的C++源碼檔案的擴充名是’.cpp’. 指定一個不同的擴充名也是可能的,隻要定義LOCAL_DEFAULT_CPP_EXTENSION變量,不要忘記開始的小圓點(也就是’.cxx’,而不是’cxx’)
include $(BUILD_SHARED_LIBRARY)
BUILD_SHARED_LIBRARY表示編譯生成共享庫,是編譯系統提供的變量,指向一個GNU Makefile腳本,負責收集自從上次調用'include $(CLEAR_VARS)'以來,定義在LOCAL_XXX變量中的所有資訊,并且決定編譯什麼,如何正确地去做。還有 BUILD_STATIC_LIBRARY變量表示生成靜态庫:lib$(LOCAL_MODULE).a, BUILD_EXECUTABLE 表示生成可執行檔案。
b. 生成.so共享庫檔案
Andro檔案已經編寫好了,現在可以用android NDK開發包中的 ndk-build腳本生成對應的.so共享庫了,方法如下:
braincol@ubuntu:~/workspace/android/NDK/hello-jni/jni$ cd ..
braincol@ubuntu:~/workspace/android/NDK/hello-jni$ ls
AndroidManifest.xml assets bin default.properties gen jni libs obj res src
braincol@ubuntu:~/workspace/android/NDK/hello-jni$ ndk-build
Gdbserver : [arm-linux-androideabi-4.4.3] libs/armeabi/gdbserver
Gdbsetup : libs/armeabi/gdb.setup
Install : libhello-jni.so => libs/armeabi/libhello-jni.so
可以看到已經正确的生成了libhello-jni.so共享庫了, 我們去 libs/armeabi/ 目錄下看看:
braincol@ubuntu:~/workspace/android/NDK/hello-jni$ cd libs/
braincol@ubuntu:~/workspace/android/NDK/hello-jni/libs$ ls
armeabi
braincol@ubuntu:~/workspace/android/NDK/hello-jni/libs$ cd armeabi/
braincol@ubuntu:~/workspace/android/NDK/hello-jni/libs/armeabi$ ls
gdbserver gdb.setup libhello-jni.so
4)在eclipse重新編譯HelloJni工程,生成apk
eclipse中重新整理下HelloJni工程,重新編譯生成apk,libhello-jni.so共享庫會一起打包在apk檔案内。在模拟器中看看運作結果。
(注:如果上述mk檔案編譯提示Android NDK: /home/long/urza/workspace/MyHelloJni/jni/Android.mk:hello-jni: LOCAL_MODULE_FILENAME should not include file extensions
Android NDK: /home/long/urza/workspace/MyHelloJni/jni/Android.mk:hello-jni: LOCAL_MODULE_FILENAME must not contain a file extension 錯誤的時候,可以顯式指定一下LOCAL_MODULE_FILENAME...如下:
LOCAL_PATH:=$(call my-dir)
include $(clear_VARS)
LOCAL_MODULE:=hello-jni
LOCAL_MODULE_FILENAME:=libhello-jni
LOCAL_SRC_FILES:=hello-jni.c
include $(BUILD_SHARED_LIBRARY)
)