1.jni的基本工作原理
(1)java的本質
想搞明白jni的本質,還要從java的本質說起.從本質上來說,java這門語言就是一門腳本語言(這是偶的個人理解,希望java大俠們不要用板磚拍我),它的運行完全依賴於腳本引擎對java的代碼進行解釋和執行(當然了,現代的java已經先進許多,可以從源代碼編譯成.class之類的中間格式的二進制文件,這種處理會大大地加快java腳本的運行速度,但是基本的執行方式仍然不變,由腳本引擎(我們稱之為JVM)來執行,與python、perl之類的純腳本相比,它只是把腳本變成了二進制格式而已.另外就是java本身對面向對象的概念支持得很好,擁有完善的功能庫可供調用,把這個腳本引擎移植到所有平台上,那麼這個腳本自然就實現所謂的“跨平台”了).絕大多數的腳本引擎都支持一個很顯著的特性,就是可以通過c/c++編寫模塊,在腳本中調用這些模塊,以此來類比java,也是一樣的,java一定要提供一種在腳本中調用c/c++編寫的模塊的機制,才能稱得上是一個相對完善的腳本引擎.
(2)android中的java
android平台從本質上是 由arm-linux操作系統 和一個叫做dalvik的java虛擬機組成的.所有在android模擬器上面看到的那些華麗的界面,都是用java語言編寫的(參見android平台源代碼的frameworks/base目錄).目前看來dalvik只是提供了一個標准的支持jni調用的java虛擬機環境.android平台中所有的硬件相關的操作均是采用jni技術進行了封裝,由java去調用jni模塊,而jni模塊使用c/c++調用android本身的arm-linux底層驅動.
例如,frameworks/base/libs/ui目錄下面有一個叫做“EGLDisplaySurface.cpp”的文件,裡面的:
status_t EGLDisplaySurface::mapFrameBuffer()函數中,就有直接對android的arm-linux中的framebuffer的初始化代碼.
這也更加印證了,android其實是依靠java+jni建立起來的王國.hoho,如此一來,就凸顯出jni在android開發中的重要性(當然,一些簡單的小程序是完全可以只用java就搞定的).
“jni”的子目錄,這個目錄將用來存放.c的文件.
(3)編寫jni模塊的java調用類
這是必然的了,jni嘛,一定要有調用者才能夠工作在src的最內層目錄裡面添加一個叫做JniModule.java的原文件,看上去如下所示:
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中,偶們可以如下調用這個類:
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庫。
生成java程序與c程序的接口文件
談到這裡,自然就會聯想到是c語言的。h文件了,現在的問題在於如何從。java文件生成我們需要的。h格式的c/c++文件。答案就是javah這個小工具基本上所有的jdk都會提供:
javah -classpath "java類的地址" <你的java模塊位置>
利用javah就可以很容易地將JniModule。java代碼的native標記的部分轉換為c/c++的。h文件中定義的導出函數。
以下是偶用於測試的makefile,相信懂makefile語法的朋友可以很容易就看明白偶在做什麼,
為了實驗能夠非常“精確”地進行,在這個makefile中的全部路徑都采用了絕對路徑,其實用相對路徑也是可以的。
java代碼:
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上工作.
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文件中,然後就是直接安裝就好了!非常簡單方便.
關於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>之前就可以了:
java代碼:
<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裡面去了。
最後需要說的就是,在偶傳上來的代碼中,可能會發現有一個叫做libcutils.a的編譯好的靜態庫,這個東西就“說來話長”了,主要原因是偶在做實驗的時候,還沒有ndk發布出來,android手機裡面也沒個gdbserver之類的工具,調試起來十分痛苦。偶認為再怎麼弱,也要輸出點東西到logcat吧?!因此,從android-platform的平台源代碼中提取了cutils的頭文件,直接把android平台編譯出來的二進制.a文件拷貝出來,鏈接到偶自己的“土法”生成的so庫裡面,這樣就可以調用libcutils.a中定義的log函數,就可以直接通過聯機的logcat查看jni中的log日志輸出,很爽!ndk的文檔中承諾,在未來的android ndk開發包中會提供在線調試的功能。
到此為止,“土法”編譯和編寫jni的方法已經基本記錄和講解完畢。一定能夠對ndk的本質有了新的認識。而不是那裡面readme和howto文檔中的幾行字,修改android。mk之類雲雲。。。
當然有了上面的這些底層編譯的探索,加上ndk裡面提供的。h和若干運行時庫,甚至android平台源代碼裡面編譯出來的靜態二進制包,jni幾乎可以實現任何功能。
還是那句話,“潘多拉”的盒子一旦打開,能否控制得住,就不是google這樣的公司能夠左右的了。
等有時間再來寫寫關於使用google的ndk來編寫和調試jni模塊的方法。。。
其實主要是關於ndk的一些編譯選項的研究和翻譯(其實人家google的文檔已經說的很清楚了)。偶選用的測試環境是slackware 12。0 + android 1。5 r1 for linux + jdk 1。6。0_12,ndk選用的是android 1。5 ndk r1這個版本的(直接解壓就行,免安裝的)。
1、從ndk安裝說起
ndk安裝的時候需要運行一個~/android-ndk-1。5_r1/build/目錄下面的一個叫做host-setup。sh的腳本。大略讀了一下這個腳本,發現這個主要是用來生成out/host/host/config。mk文件的。主要用於指定用戶操作系統的判斷以及支持的編譯器類型(設置makefile中的cc,ar,ld之類的變量)
ndk的目錄介紹。
2、ndk的目錄結構分析
進入android-ndk-1。5_r1目錄,看到如下目錄結構:
GNUmakefile: 標准的makefile格式的文件,用於引用build/core/main。mk的編譯腳本。
README。TXT:基本的說明,沒啥大用,真正有用的文檔都在docs目錄下面。
apps/:存放帶有jni接口的android工程目錄(工程裡面有利用native關鍵字定義的java函數)
build/:存放著幾乎所有的ndk編譯相關的腳本以及必要的靜態鏈接庫。
docs/:存放這ndk的所有“官方”文檔,每一篇文檔對於jni編寫者來說這裡面的任何一點點資料都是無價的。
out/:存放一些中間的臨時文件,例如jni的。c/。cpp文件編譯過程中產生的。o文件等。
sources/:存放jni文件的。c/。cpp的源代碼文件。
3、基本的使用方法
(1)創建一個android工程
進入apps目錄,運行如下命令:
android create project --target 2 --package com。TWM --activity NDKTest --path 。/NDKTest/project
通過命令行創建一個叫做NDKTest的activity,注意這裡的--path需要設置為。/XXXXX/project這個目錄,這個XXXXX目錄主要是為了ndk的make區分不同項目和工程使用的。編寫Application。mk文件的時候,一定要把Application。mk寫到這個XXXXX目錄下面。
$NDK/apps/<myapp>/Application。mk
另外,編譯jni庫的時候使用的命令也是如此:
make APP=<your app name>
這裡的<your app name>實際上也是這個XXXXX目錄。
(2)為工程添加一個jni的java調用接口
進入app/NDKTest/project/src/com/TWM/NdkTest目錄,建立一個新的java文件(例如:NDKJni。java),然後把代碼寫成類似下面這個樣子:
java代碼:
package com.TWM.NdkTest ;
public class NDKJni {
public native int MyFunc(int a, int b) ;
static {
System.loadLibrary("NDKjni") ;
}
}
復制代碼
這裡的MyFunc由於是使用native修飾,因此,這個MyFunc函數是一個調用jni的函數。
(3)為java工程編寫Application。mk文件
該文件主要放在app/NDKTest目錄下,用於告知ndk的編譯腳本,當前的程序需要哪個jni模塊。
看上去應該是這個樣子的:
APP_PROJECT_PATH := $(call my-dir)/project ---> 當前目錄下的project目錄包含了jni模塊的java接口
APP_MODULES := NDKTest --->當前模塊的名字叫做NDKTest
(4)弄清楚java程序的包層次
以當前的這個project為例,就是上面代碼中的package com。TWM。NdkTest,定義的類名為NDKJni。因此,根據這個包的層次,可以根據jni文件的函數命名規則定義函數:
JNIEXPORT jint JNICALL Java_com_TWM_NdkTest_NDKJni_MyFunc(JNIEnv * env,jobject thiz,jint a,jint b) ;
當然,手工根據包層次定義jni函數還是很痛苦的,可以借助於javah工具:
mkdir -p apps/NDKTest/project/jni
cd apps/NDKTest/project/jni
javah -classpath "/bin/classes" com。TWM。NdkTest。NDKJni
然後就會自動生成一個叫做com_TWM_NdkTest_NDKJni。h的文件,裡面的內容基本上跟手工生成的差不多:
java代碼:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_TWM_NdkTest_NDKJni */
#ifndef _Included_com_TWM_NdkTest_NDKJni
#define _Included_com_TWM_NdkTest_NDKJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_TWM_NdkTest_NDKJni
* Method: MyFunc
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_com_TWM_NdkTest_NDKJni_MyFunc
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
作者:t80t90s