天天看點

java 調用 c

下面我就用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 =&gt; 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)

)