android平台中編寫jni模組的方法(2)

來源:互聯網
上載者:User

繼續上一篇,目前android平台的sdk已經發布到了cupcake 1.5的版本(最新的開發版可能要比這個版本更高,期待android 2.0 :D)

對 於android 1.5版本的開發人員而言,一個非常大的好訊息是cupcake已經開始“官方”支援開發人員編寫自己的jni庫了,這主要表現在google放出了一個叫做 android-ndk的開發包,這個開發包是專門為了開發jni而準備的一些必要的標頭檔以及一些運行時所需的庫,為android應用開發人員提供了比 較方便的指令碼支援。這種方便和快捷是在android 1.0和1.1的sdk以及配套的工具裡面是沒有的。試用了ndk以後感覺良好,偶對於之前沒有使用ndk,純粹採用第三方編譯工具進行jni開發嘗試的 “土法”進行一次歸納和整理,並且發布一個偶做測試用的jni代碼。

從ndk的聲明裡面偶可以看到,google在使用jni技術進行開發的時候提出的幾點需要主意的地方:
The NDK is *not* a good way to write generic native code that runs on Android
devices. In particular, your applications should still be written in the Java
programming language, handle Android system events appropriately to avoid the
"Application Not Responding" dialog or deal with the Android application
life-cycle.

Note however that is is possible to write a sophisticated application in
native code with a small "application wrapper" used to start/stop it
appropriately.

A good understanding of JNI is highly recommended, since many operations
in this environment require specific actions from the developers, that are
not necessarily common in typical native code. These include:

  - not being able to directly access the content of VM objects through
    direct native pointers. E.g. you cannot safely get a pointer to a
    String object's 16-bit char array to iterate over it in a loop.

  - requiring explicit reference management when the native code wants to
    keep handles to VM objects between JNI calls.

The NDK only provides system headers for a very limited set of native
APIs and libraries supported by the Android platform. While a typical
Android system image includes many native shared libraries, these should
be considered an implementation detail that might change drastically between
updates and releases of the platform.

If an Android system library is not explicitely supported by the NDK
headers, then applications should not depend on it being available, or
they risk breaking after the next over-the-air system update on various
devices.

Selected system libraries will gradually be added to the set of stable NDK
APIs.

從 上述觀點可以看出,google對於dalvik本身的穩定性以及android的framework的信心還是很足的,所以不建議開發人員使用jni技術 (偶個人認為,他們是在擔心開發人員的水平高低不同,會影響到他們系統的穩定性),同時也告誡開發人員即使使用了jni技術也未必會大幅度提升程式的運行效率 (對於這一點偶表示懷疑,java終究是java,在vm上面晃蕩的指令碼的運行效率怎麼可能跟最佳化過的native程式相比呢?當然對於這個問題是仁者見 仁,智者見智了。)

總之,雖然google提供了ndk可以較為方便地構建android平台的jni模組,但是作為對於凡事喜歡刨根問底的偶來說,光知道運行幾個指令碼跟不懂沒有任何區別,偶希望的是從原理上去瞭解android的jni技術。

於是,開始了這次“土法”建jni的過程:
(1)工具鏈的準備工作
第一步,當然是jdk以及android的sdk這些就不再贅述了。
第二不,登陸http://www.codesourcery.com的download頁面,下載arm-2008q3-72-arm-none-linux-gnueabi.bin這個編譯工具(偶是linux環境,不同的os可能稍微有些區別)
然後就是安裝,設定環境變數,偶把自己的環境變數設定貼出來備份一下:
# for midnight commander default editor, i hate vi or vim (sorry for vi-ers)
export EDITOR=emacs
export VIEWER=emacs

# for splint
export LARCH_PATH=/usr/local/share/splint/lib
export LCLIMPORTDIR=/usr/local/share/splint/imports
export PATH=/usr/local/bin/splint:${PATH}

# for android jni developments
export PATH=/home/wayne/CodeSourcery/Sourcery_G++_Lite/bin:${PATH}

# for android sdk
export PATH=/home/wayne/android-sdk-linux_x86-1.5_r1/tools:${PATH}
export PATH=/home/wayne/jdk1.6.0_12/bin:${PATH}
export JAVA_HOME=/home/wayne/jdk1.6.0_12
export ANDROID_JAVA_HOME=${JAVA_HOME}
export ANDROID_HOME=/home/wayne/android-sdk-linux_x86-1.5_r1

# for symbian s60 v3 dev
export PATH=/home/wayne/epoc_sdk/s60_31_fp1/tools_wrapper:$PATH
export EPOCROOT=/home/wayne/epoc_sdk/s60_31_fp1/

# for android platform codes
export ANDROID_PRODUCT_OUT=/home/wayne/works/android_source/out/target/product/generic

# for android ndk
export ANDROID_NDK_ROOT=/home/wayne/android-ndk-1.5_r1/

# for apache ant
export PATH=/usr/local/apache-ant-1.7.1/bin:${PATH}
export ANT_HOME=/usr/local/apache-ant-1.7.1

# for splint
export LARCH_PATH=/usr/local/share/splint/lib
export LCLIMPORTDIR=/usr/local/share/splint/import

# for git
export PATH=/usr/local/bin:${PATH}

(2)建立JniTest的項目
具體的命令列建立過程,請參考第一篇文章的方法。
在JniTest的目錄下建立一個叫做“jni”的子目錄,這個目錄將用來存放.c的檔案。

(3)編寫jni模組的java調用類
這是必然的了,jni嘛,一定要有調用者才能夠工作在src的最內層目錄裡面添加一個叫做JniModule.java的原檔案,看上去如下所示:
public class JniModule {
    static {
        System.loadLibrary("aaaa") ;
    }
    public native static int jni_add(int a, int b) ;
}
注 意,偶們最終會產生一個叫做libaaaa.so的arm相容的二進位動態庫,但是在使用System.loadLibrary動態載入的時候,只需要填 寫lib和.so之間的名字aaaa即可,在此實驗的功能僅僅是兩個數字a和b的求和計算以及如何在jni的c語言模組中把log日誌列印到logcat 中。

在JniTest.java中,偶們可以如下調用這個類:
    public void onClick(View v) {
        String ss ;
        int a = 3 ;
        int b = 4 ;
        
        ss = "" ;
        switch(v.getId()) {
        case R.id.button1:
            ss = "a="+String.valueOf(a)+","+"b=" + String.valueOf(b) + "," + "a+b=" +
                String.valueOf(JniModule.jni_add(a, b)); 
            setTitle(ss) ;
            break ;
        case R.id.button2:
            setTitle("button2 click") ;
            break ;
        case R.id.button3:
            int pid = android.os.Process.myPid();
            android.os.Process.killProcess(pid);
            break ;
        }   
    }
注意,這裡的button3是很重要的,功能是得到當前程式的進程id,然後顯示地殺掉它!
為 什麼要這麼做呢?原因在於,android裡面的常規退出函數並沒有真正地關閉當前啟動並執行進程,而是切換到後台去了。這對普通的java應用看上去很平 常,而且可以加速再次啟動該程式的速度,但是對於帶有jni模組的java程式而言就是惡夢,因為程式沒有真的關閉。所以那個libaaaa.so庫,會 一直停留在記憶體中,這時候如果你希望把舊的so庫替換成新的庫,那就要重啟手機才行。。。很痛苦,所以想到了這種辦法,直接殺掉自己,那麼下一次啟動的時 候就會自動重新載入最新的so庫。

(4)產生java程式與c程式的介面檔案
談到這裡,自然就會聯想到是c語言的.h檔案了,現在的問題在於如何從.java檔案產生我們需要的.h格式的c/c++檔案。答案就是javah這個小工具基本上所有的jdk都會提供:
javah -classpath "java類的地址" <你的java模組位置>
利用javah就可以很容易地將JniModule.java代碼的native標記的部分轉換為c/c++的.h檔案中定義的匯出函數。

以下是偶用於測試的makefile,相信懂makefile文法的朋友可以很容易就看明白偶在做什麼,
為了實驗能夠非常“精確”地進行,在這個makefile中的全部路徑都採用了絕對路徑,其實用相對路徑也是可以的(省力多了,但在做實驗的時候要求絕對正確無誤。。。):
CC=arm-none-linux-gnueabi-gcc
LD=arm-none-linux-gnueabi-ld
MV=mv
JH=javah
JHFLAGS=-classpath "/home/wayne/works/workspace/JniTest/bin"
LDFLAGS=-T "/home/wayne/CodeSourcery/Sourcery_G++_Lite/arm-none-linux-gnueabi/lib/ldscripts/armelf_linux_eabi.xsc" -shared
CFLAGS=-I. -I/home/wayne/works/workspace/JniTest/jni/include -I/home/wayne/works/workspace/JniTest/jni/include/linux -I/home/wayne/works/workspace/JniTest/jni -fpic

all: libaaaa.so

com_hurraytimes_jnitest_JniModule.h:
    $(JH) $(JHFLAGS) com.hurraytimes.jnitest.JniModule

aaaa.o: aaaa.c com_hurraytimes_jnitest_JniModule.h
    $(CC) $(CFLAGS) -c -o aaaa.o aaaa.c

libaaaa.so: aaaa.o
    $(LD) $(LDFLAGS) -o libaaaa.so aaaa.o libcutils.a
    $(RM) ../libs/armeabi/libaaaa.so
    $(MV) libaaaa.so ../libs/armeabi/

clean:
    $(RM) *.o *.so *~

這 裡需要特別提一點的,就是關於arm-none-linux-gnueabi-gcc的使用問題,這個編譯器自從到了2008版本就開始琢磨著實現更加方 便地“cross compiler”的功能了。以往的版本是arm-xxx-linux-gcc,就是為了編譯arm-linux平台的軟體的,如果你的晶片從三星的變為 菲利普的,那麼整條工具鏈就要重新編譯。現在的這個2008版的為了讓廣大開發人員(尤其是多種不同晶片平台的嵌入式開發人員)的電腦裡面不要安裝好多套 for 不同晶片集的gcc工具鏈,弄了一個-T的參數,這裡就可以讓開發人員使用一個gcc 工具鏈產生不同平台和格式的可執行代碼以及連結的庫。雖然如此,但是偶還是覺得不大習慣,總之謝謝CodeSourcery很貼心的功能,讓偶花了半個多 小時在琢磨和查資料,到底是什麼原因導致產生的jni模組無法在android上工作。

(5)jni模組的打包問題
再次聲明,在android 1.5 cupcake以後的版本才可以用偶下文提到的打包方法。
在 查看了ndk的指令碼以後,我才知道原來android 1.5版本在打包apk的時候,是完全可以支援直接將.so的jni庫打包到apk安裝包中去的,解決了偶們這種鐵杆c/c++開發人員開發自己的jni組 件的發布問題,java指令碼嘛,做個事件啥的中轉就完成它的使命了。

其實具體操作起來非常簡單,在當前項目的跟目錄下建立如下目錄:
/libs/armeabi
然後把自己產生好的so庫拷貝到這個armeabi目錄下面即可,運行ant產生apk發布包的時候,就會自動地將/libs/armeabi目錄下的so庫打包到apk檔案中,然後就是直接安裝就好了!非常簡單方便。
(6)關於ant裡面實現jni的makefile調用的方法
首先肯定一下,ant是個不錯的東西。但是如果說它要取代makefile的地位,偶個人固執地認為很難。makefile文法簡單,隨手就可以敲一個,但回頭看看ant的build.xml,第一眼看上去就頭暈。
xml很不錯,但是就是他大爺的亂七八糟,而且居然宣稱說是給人看的東西。。。凡事真正有些實質性的用處,用xml儲存的資料(用於示範hello world之類的xml就免了),讓人看起來都會頭暈。

ant採用xml作為基本輸入,偶個人認為還不如仿效makefile弄一套相對簡單的文法來得方便。
好了不再發牢騷了,開始看一下,如何為android的build.xml添加ant支援的xml實現自動調用jni的makefile檔案。
以下是偶用ant來編譯jni模組的xml,稍加修改就可以用於開發和實驗中,把這些加到</project>之前就可以了:
    <target name="mk" >
    <exec dir="./jni" executable="make" os="Linux" failonerror="true">
    </exec>
    </target>

    <target name="mkclr" >
    <exec dir="./jni" executable="make" os="Linux" failonerror="true">
    <arg line=" clean" />
    </exec>
    </target>
使用方法就是ant mk和ant mkclr一個是相當於調用make,另一個是相當於調用make clean。
其餘的操作都放到makefile裡面去了(儘管偶的一位java朋友告訴我,makefile能做的事情ant都能做,makefile不能做的事情ant也能做,偶還是傾向於用makefile。。。除了頑固不化以外,最重要的一個原因是──懶得敲那麼多東西。)。

最 後需要說的就是,在偶傳上來的代碼中,可能會發現有一個叫做libcutils.a的編譯好的靜態庫,這個東西就“說來話長”了,主要原因是偶在做實驗的 時候,還沒有ndk發布出來,android手機裡面也沒個gdbserver之類的工具,調試起來十分痛苦。偶認為再怎麼弱,也要輸出點東西到 logcat吧?!因此,從android-platform的平台原始碼中提取了cutils的標頭檔,直接把android平台編譯出來的二進位.a 檔案拷貝出來,連結到偶自己的“土法”產生的so庫裡面,這樣就可以調用libcutils.a中定義的log函數,就可以直接通過聯機的logcat查 看jni中的log日誌輸出,很爽!ndk的文檔中承諾,在未來的android ndk開發包中會提供線上調試的功能(gdbserver嗎?呵呵,有了gdb,我想他們想要完全控制android已經不太現實了,畢竟gdb太強大 了。。。)

到此為止,“土法”編譯和編寫jni的方法已經基本記錄和講解完畢。相信能夠耐著性子看完偶這篇文章的朋友,一定能夠對ndk的本質有了新的認識。而不是那裡面readme和howto文檔中的幾行字,修改android.mk之類云云。。。

當然有了上面的這些底層編譯的探索,加上ndk裡面提供的.h和若干執行階段程式庫,甚至android平台原始碼裡面編譯出來的靜態二進位包,jni幾乎可以實現任何功能。

還是那句話,“潘多拉”的盒子一旦開啟,能否控製得住,就不是google這樣的公司能夠左右的了。
等有時間再來寫寫關於使用google的ndk來編寫和調試jni模組的方法。。。

我珍貴的時間啊。。。眼看著生命在亂七八糟的代碼中慢慢度過。。。

附件是JniTest的全部測試代碼,希望能夠對各位android的探索者有所協助。

http://blog.chinaunix.net/link.php?url=http://blogimg.chinaunix.net%2Fblog%2Fupfile2%2F090709115111.gz

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.