轉(zhuǎn)載地址:http://blog.chinaunix.net/space.php?uid=8272118&do=blog&id=2033361
一、寫在前面的話前幾篇文章中用于驗(yàn)證目的而編寫的代碼都是基于linux平臺(tái)和sdl圖形庫的,雖然效果很好很強(qiáng)大(偶在mac的環(huán)境下也做了類似的實(shí)驗(yàn),確實(shí)不錯(cuò)),但是這些實(shí)驗(yàn)畢竟是基于pc環(huán)境的,沒有在真正的手機(jī)arm環(huán)境下面跑跑總覺得心里沒有多大的把握。雖然google io大會(huì)上曾經(jīng)說過,android2.2瀏覽器使用的webkit內(nèi)核就是使用了v8引擎來加速瀏覽器的運(yùn)行速度,偶在手機(jī)上刷了個(gè)2.2的包,測(cè)試了一下瀏覽器,也沒有感覺到如何如何地快(偶只是隨便按了幾下,可能不足以說明問題)。
最后,偶還是決定要?jiǎng)觿?dòng)手,把v8引擎弄到真機(jī)上運(yùn)行一下,中國有句俗話“是騾子是馬,牽出來遛遛”。
本文的以下部分會(huì)非常枯燥,主要記錄了偶將v8引擎封裝成一個(gè)jni庫的全部過程,然后通過java去調(diào)用(推崇java的朋友們不要拍偶,說實(shí)話,偶一直把java語言本身當(dāng)成是腳本的一種)。
二、實(shí)驗(yàn)方法記錄1、把v8引擎單獨(dú)編譯成一個(gè).a或者.so文件原本希望能夠把v8引擎的源代碼直接編譯到j(luò)ni的源代碼里面,但是后來發(fā)現(xiàn)既然android從2.0開始就有這東西的話,干嘛不拿來直接用呢?簡(jiǎn)單地搜索了一下android源代碼(偶用的是2.1eclair的源代碼環(huán)境,但是郁悶的是編譯出來的程序不可以在1.5和1.6的環(huán)境下工作,具體原因偶還需要查一查),發(fā)現(xiàn)在$(ANDROID_SRC_ROOT)/external/webkit/目錄下面,有一個(gè)叫做V8Binding的目錄,這里面就有一個(gè)叫做v8的目錄,該目錄下面就是一個(gè)完整的v8引擎了,從其他的諸如binding以及jni的目錄來看,android的開發(fā)者們也是一直在琢磨著把v8庫編譯成一個(gè)so或者一個(gè).a文件,把接口暴露給上層模塊,通過jni的方式去調(diào)用?;氐缴弦患?jí)目錄,打開Android.mk文件,呵呵,原來如此,編譯腳本進(jìn)行編譯的時(shí)候是需要看到一個(gè)叫做JS_ENGINE的環(huán)境變量的,如果該變量取值為“v8”,則將v8引擎編譯成.a文件,然后鏈接到webkit里面去,如果JS_ENGINE的取值是“jsc”,那么編譯腳本將使用webkit內(nèi)置的JavaScriptCore中的代碼來生成腳本引擎。
通過查看Android.mk文件知道了如何讓編譯環(huán)境生成libv8.a的方法以后,剩下的事情就簡(jiǎn)單多了。
$cd ~/android-src
$export JS_ENGINE=v8
$make
然后,就可以出去散散心了,整個(gè)編譯過程會(huì)非常地漫長(大概有45分鐘左右吧?!)
散步歸來,發(fā)現(xiàn)已經(jīng)make好了。那么就要找一找這個(gè)libv8.a或者libv8.so到底放在什么地方了。
最后,偶找到了這個(gè):
$(ANDROID_SRC_ROOT)/out/target/product/generic/obj/STATIC_LIBRARIES/libv8_intermediates/libv8.a
如此說明,v8引擎的靜態(tài)庫已經(jīng)成功地生成出來了!好,下一步,就要想辦法把這個(gè).a文件鏈接到j(luò)ni庫里面去了。
查了一下ndk的文檔,在android-mk.txt文檔中找到了這樣一個(gè)參數(shù):
LOCAL_STATIC_LIBRARIES
The list of static libraries modules (built with BUILD_STATIC_LIBRARY)
that should be linked to this module. This only makes sense in
shared library modules.
看這個(gè)幫助的說明,似乎是說這里面的staticlibrary是要用BUILD_STATIC_LIBRARY編譯成的才行。偶剛剛已經(jīng)在android的源碼編譯過程中拿到了libv8.a,那么在這里似乎不能夠直接用這個(gè)參數(shù)。于是,偶決定放棄ndk,既然libv8.a是用android的源代碼編譯出來的,那么就用源代碼中提供的工具鏈來生成jni吧,android自己的代碼生成的.a靜態(tài)庫,用它自己的工具鏈總可以完成鏈接吧?!
2、用sdk開始搭建一個(gè)簡(jiǎn)單的android應(yīng)用程序框架jni和java端的調(diào)用程序,這兩者正常關(guān)系是先編寫java包裝類,然后利用javah生成jni相關(guān)定義的頭文件。偶在這里也遵循這個(gè)順序來一步一步地走。無論是用eclipse還是用android命令行腳本生成一個(gè)項(xiàng)目都可以,既然作實(shí)驗(yàn)就要做得像真的一樣,偶把測(cè)試項(xiàng)目中的普通TextView給換成了SurfaceView(畢竟效率要高一些嘛)。然后,就是編寫一個(gè)簡(jiǎn)單的wrapper類,導(dǎo)出了3個(gè)native的java函數(shù),這三個(gè)函數(shù)是需要用jni去實(shí)現(xiàn)的。
偶只把java代碼貼在這里,不做更多說明:
public class V8Wrapper {
private static final String TAG = "V8Wrapper";
protected SurfaceHolder m_surf_holder ;
protected byte [] m_js_source ;
V8Wrapper(SurfaceHolder surf_holder) {
m_surf_holder = surf_holder ;
m_js_source = null ;
}
public String V8GetVersion() {
return v8_get_version() ;
}
public boolean V8LoadJS(String js_fname) {
FileInputStream fis = null ;
FileChannel fc = null;
ByteBuffer bb = null ;
int flen = 0 ;
try {
fis = new FileInputStream(js_fname) ;
} catch(FileNotFoundException e) {
Log.e(TAG, "can't open javascript file : " + js_fname) ;
return false ;
}
fc = fis.getChannel() ;
try {
flen = (int)(fc.size());
bb = ByteBuffer.allocate(flen);
fc.read(bb);
} catch(IOException e) {
Log.e(TAG, "ByteBuffer.allocate() or fc.read() failed, fc.size=" + String.valueOf(flen)) ;
return false ;
}
bb.flip() ;
m_js_source = null ;
m_js_source = bb.array() ;
return true ;
}
public boolean V8RunJS() {
if(m_js_source == null) {
return false ;
}
return v8_run_js(m_js_source) ;
}
public boolean V8OnTouch(MotionEvent event) {
if(m_js_source == null) {
return false ;
}
return v8_on_touch(m_js_source, (int)(event.getX()), (int)(event.getY())) ;
}
private native String v8_get_version() ;
private native boolean v8_on_touch(byte [] js_source, int x, int y) ;
private native boolean v8_run_js(byte [] js_source) ;
static {
System.loadLibrary("v8wrapper") ;
}
}
這里的v8_get_version()函數(shù),只是用來驗(yàn)證能否從jni函數(shù)中調(diào)用libva.a的函數(shù)用的,以后的實(shí)現(xiàn)中可以忽略,畢竟知道自己調(diào)用的v8引擎的版本號(hào)也是好的。
3、開始編寫jni模塊(1)一些準(zhǔn)備工作$cd $(ANDROID_SRC_ROOT)
$. build/envsetup.sh
$mkdir external/myapps/v8wrapper
$cd external/myapps/v8wrapper
ok,現(xiàn)在可以開始要做的事情了,第一件事情自然是編寫makefile了,創(chuàng)建Android.mk文件,看上去像下面這樣:
ifneq ($(TARGET_SIMULATOR),true)
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES:= v8wrapper.cpp
LOCAL_C_INCLUDES += $(JNI_H_INCLUDE) \
$(LOCAL_PATH)/../../webkit/V8Binding/v8/include \
$(LOCAL_PATH)/../../skia/include \
$(LOCAL_PATH)/../../skia/include/core \
$(LOCAL_PATH)/../../skia/include/images \
$(LOCAL_PATH)/../../skia/include/graphics \
$(LOCAL_PATH)/../../skia/include/utils
LOCAL_CFLAGS +=-O0
LOCAL_MODULE := libv8wrapper
LOCAL_SHARED_LIBRARIES := libcutils libskia
LOCAL_STATIC_LIBRARIES := libv8
LOCAL_PRELINK_MODULE := false
include $(BUILD_SHARED_LIBRARY)
endif # TARGET_SIMULATOR != true
需要提幾句的地方:
(a) LOCAL_PRELINK_MODULE := false這個(gè)屬性一定要有,因?yàn)榕荚诖俗鰧?shí)驗(yàn)的jni是不再“正常”的android系統(tǒng)中的,android系統(tǒng)源代碼在編譯的過程中會(huì)自動(dòng)忽略此jni。為了告訴android的編譯系統(tǒng)此模塊不在原先正常的系統(tǒng)中,但是也要編譯出來,所以只好把這里寫上這么一個(gè)屬性了。
(b)LOCAL_SHARED_LIBRARIES := libcutils libskia鏈接libcutils.so的目的在于偶要使用android系統(tǒng)的log函數(shù),而鏈接libskia.so的目的在于,整個(gè)android系統(tǒng)的ui框架,其基礎(chǔ)都是建立在skia這個(gè)圖形庫的前提之下的,android的framework在繪制ui的過程中大量使用了jni模塊去調(diào)用skia庫的功能,既然偶自己可以通過c\c++代碼直接調(diào)用skia庫,那我干嘛還要java層去做這樣的事情呢?腳本引擎的效率肯定要比二進(jìn)制代碼低,因此,偶在jni里面選擇了盡量不去麻煩android框架去做一些在jni里面就能夠做到的事情(例如,屏幕繪圖,圖片解碼、貼圖等操作,本著能省則省的原則來避免不必要的性能浪費(fèi))。
在這里插一嘴,在android框架代碼的Canvas.h文件中($(ANDROID_SRC_ROOT)/frameworks/base/graphics/java/android/graphics/Canvas.h),可以看到如下的定義:
final int mNativeCanvas;
經(jīng)過進(jìn)一步的代碼追蹤,偶發(fā)現(xiàn)這個(gè)所謂的mNativeCanvas實(shí)際上就是指向skia庫中的一個(gè)SkCanvas對(duì)象的指針。(java里面沒有指針數(shù)據(jù)類型,為了保留這個(gè)指針,android框架的設(shè)計(jì)者不得已在這里使用了int數(shù)據(jù)類型在java環(huán)境中保存c/c++環(huán)境的指針,偶不是第一個(gè)發(fā)現(xiàn)這個(gè)小秘密的,已經(jīng)有臺(tái)灣的高人發(fā)現(xiàn)過了,在此偶只是強(qiáng)調(diào)一下)既然知道了這一點(diǎn),偶在jni的實(shí)現(xiàn)中,得到j(luò)ava環(huán)境的Canvas對(duì)象以后,只需要把Canvas對(duì)象的mNativeCanvas屬性中的數(shù)據(jù)進(jìn)行強(qiáng)制類型轉(zhuǎn)換就可以很方便地得到android系統(tǒng)中指向SkCanvas對(duì)象的指針,剩下的所有屏幕操作都可以籍由這個(gè)指針,結(jié)合skia庫來完成,何勞dalvik大駕?!呵呵,根據(jù)偶最后在真機(jī)上的運(yùn)行結(jié)果來看,上面的一點(diǎn)點(diǎn)小hack確實(shí)對(duì)jni中提升屏幕繪制效率有很大幫助。(上面這一段文字很不好寫,更不好懂,感興趣的朋友可以多讀幾遍)
在jni里與此相關(guān)的函數(shù)如下:
static SkCanvas * get_skcanvas(JNIEnv * env, const jobject & canvas_obj) {
const char * tag = "jni:get_skcanvas, " ;
jclass cls_canvas ;
jfieldID fid_native_canvas ;
cls_canvas = env->GetObjectClass(canvas_obj) ;
// 大家注意了,這一句就是獲取mNativeCanvas屬性的存儲(chǔ)位置id
fid_native_canvas = env->GetFieldID(cls_canvas, "mNativeCanvas", "I") ;
if(fid_native_canvas == NULL) {
log(tag, "lookup mNativeCanvas in Canvas class failed!") ;
return (SkCanvas *)(NULL) ;
}
// 這一句就通過GetIntFiled函數(shù)把里面的數(shù)據(jù)取出來,然后強(qiáng)制類型轉(zhuǎn)換即可
return (SkCanvas *)(env->GetIntField(canvas_obj, fid_native_canvas)) ;
}
其他的語法都是jni的常規(guī)語法,在此偶就不再聒噪了。
(c)LOCAL_STATIC_LIBRARIES := libv8這個(gè)就不用偶多說了,libv8.a既然生成出來了,就不要浪費(fèi),自然需要鏈接進(jìn)來。(誰知盤中庫,庫庫皆辛苦。。。)
(d) $(LOCAL_PATH)/../../webkit/V8Binding/v8/include \此include路徑是v8引擎在android 2.1源代碼中的位置,此時(shí),v8引擎是作為webkit的一部分存在的;而在android 2.2源代碼以后,v8引擎的位置將獨(dú)立于webkit,自成一體,這時(shí)候的include路徑應(yīng)該改為:
$(LOCAL_PATH)/../../v8/include \
也就是說android 2.2源代碼編譯環(huán)境把v8目錄從webkit目錄內(nèi)拿出來了。
(2)編寫jni模塊編寫過程是一個(gè)非常讓人郁悶的過程(尤其是偶采用的是android源代碼工具鏈進(jìn)行編譯,而ndk-r4中自帶的調(diào)試功能不可以使用,但是畢竟android源代碼作為編譯環(huán)境可以調(diào)用很多android內(nèi)部的library,這一點(diǎn)是要比ndk爽很多地。。。)。
在這里需要說明一下SurfaceView中進(jìn)行屏幕繪制的關(guān)鍵代碼(java版本的):
try {
m_canvas = m_surf_holder.lockCanvas(null) ;
m_canvas.drawBitmap(bmp, x, y, null) ;
} finally {
if (m_canvas != null) {
m_surf_holder.unlockCanvasAndPost(m_canvas);
}
}
在java代碼中,使用SurfaceView進(jìn)行繪圖的時(shí)候分為三步:
(a)m_canvas = m_surf_holder.lockCanvas(null) ;
首先是通過一個(gè)SurfaceHolder的對(duì)象調(diào)用lockCanvas()函數(shù),該函數(shù)會(huì)返回一個(gè)Canvas對(duì)象(就是這里的m_canvas,至于SurfaceHolder對(duì)象的獲取,可以通過調(diào)用getHolder()取得)。
(b)m_canvas.drawBitmap(bmp, x, y, null) ;
這里就是使用m_canvas進(jìn)行繪圖,貼圖操作,在這些繪圖操作進(jìn)行的過程中,所有操作都繪制到了一個(gè)內(nèi)存中的屏幕上,并不會(huì)立即顯示到當(dāng)前的屏幕上。
(c)m_surf_holder.unlockCanvasAndPost(m_canvas);
只有調(diào)用了unlockCanvaAndPost()函數(shù)以后,剛剛繪制在m_canvas上的所有內(nèi)容才會(huì)顯示在當(dāng)前屏幕上。
在偶的jni模塊實(shí)現(xiàn)中,把上述在java中的三個(gè)步驟統(tǒng)統(tǒng)地搬到了jni函數(shù)中,用c++去完成。雖然看上去麻煩一些,畢竟省得麻煩dalvik去進(jìn)行幾次jni到j(luò)ava以及java到j(luò)ni的反射過程,會(huì)提升一些執(zhí)行效率(呵呵,當(dāng)然了,也可能是偶的心里作用吧,通過這么做可以讓偶心里安慰一些??。?br>
(3)關(guān)于v8引擎的回調(diào)基本的腳本執(zhí)行,以及函數(shù)回調(diào)原理在偶的上兩篇文章中已經(jīng)講述過了,在此不再聒噪。只是偶在不知道v8的名字空間會(huì)引起jni的任何問題之前,還是沒有敢直接使用:
using namespace v8 ;
這樣的語句。所以很多在前兩篇文章中很簡(jiǎn)單的聲明,就要寫的很繁瑣了(重復(fù)地敲這些無用的東西,偶的手指都有些酸了)例如:以前的Handle<String>,現(xiàn)在就要寫成v8::Handle<v8::String>。
在此真機(jī)測(cè)試中,偶為javascript腳本導(dǎo)出了如下函數(shù):
(a)set_back_color(r, g, b):設(shè)置背景顏色,RGB格式。
(b)set_draw_color(r, g, b):設(shè)置畫筆顏色,RGB格式。
(c)set_txt_size(size):設(shè)置顯示文字的大小,支持小數(shù)點(diǎn),必須大于0。
(d)draw_line(x0, y0, x1, y1):使用畫筆顏色畫直線。
(e)clear():用背景色填充整個(gè)屏幕,顧名思義就是清屏。
(f)draw_img(x, y, fname):以(x,y)為左上角坐標(biāo)貼圖,fname為圖片名稱(圖片一定要放在v8data目錄,圖片格式支持bmp, png, jpeg, gif)
(g)draw_txt(x, y, txt):使用前請(qǐng)?jiān)O(shè)置畫筆顏色和文字大小,在(x, y)為左上角坐標(biāo)在屏幕上繪制文字。
最后,腳本支持一個(gè)OnClick(x, y)的回調(diào)函數(shù),當(dāng)屏幕被點(diǎn)按的時(shí)候,v8會(huì)自動(dòng)回調(diào)此函數(shù)。
所有這些函數(shù)在c++的實(shí)現(xiàn)中,多虧了SkCanvas提供的強(qiáng)大功能,偶只用了幾行代碼就解決了戰(zhàn)斗,skia確實(shí)名不虛傳(再次贊揚(yáng)一下skia的原創(chuàng)人員)。
感興趣的朋友可以通過改寫偶在v8data目錄下面提供的t1.js腳本文件來看看不同的運(yùn)行結(jié)果。
(4)編譯方法$ cd $(ANDROID_SCR_ROOT)/external/myapps/v8wrapper
$mm
注意,一定要在$(ANDROID_SCR_ROOT)目錄下運(yùn)行過“.build/envsetup.sh”腳本后才能夠用mm進(jìn)行編譯(這腳本方便,mm命令可以理解為——》“美眉”,讓美眉幫偶編譯,呵呵,不過這位美眉的脾氣不太好,編譯腳本稍微寫的不對(duì)就不給編譯。感慨一下,如果美眉能夠自動(dòng)理解這些,偶該多么地幸福啊!~(#@*&$(@#*&$)
編譯完成以后,需要手工把如下路徑下的文件拷貝到j(luò)ava項(xiàng)目的lib/armeabi目錄下:
$(ANDROID_SRC_out/target/product/generic/system/lib/libv8wrapper.so
畢竟沒有了ndk-build腳本的幫忙,什么事情都要自己動(dòng)手。
(5)運(yùn)行方法無論大家用ant或者eclipse都可以生成安裝包(大概515k左右,懶得編譯的朋友,可以下載偶提供的apk包)。
(a)直接adb install v8test-debug.apk安裝到手機(jī)上(模擬器上安裝必須是2.0或者更高版本的才行)
(b)通過adb shell在/sdcard下面建立一個(gè)目錄叫做v8data(注意,全部小寫的v8data目錄)
(c)把java代碼中的V8Test/v8data目錄下的所有文件通過ddms拷貝到手機(jī)sdcard上面的v8data目錄里面
(包括若干張png圖片,以及一個(gè)t1.js腳本)
(d)打開“V8TestActivity”程序,如果看到黑黑的屏幕,代表一切正常,否則代表有問題。
(e)按下手機(jī)的“menu”按鍵,選擇“l(fā)oad”,然后選擇“run”
(f)在屏幕上單擊圖標(biāo),會(huì)看到有一個(gè)白色框框跟隨走動(dòng)。
所有這些界面和效果都是用javascript編寫的,可以通過打開t1.js文件進(jìn)行修改,然后覆蓋到sdcard上的v8data目錄下即可。連程序都不需要退出,只要重新點(diǎn)按一下"load"和"run"即可看到修改后的結(jié)果。
是不是很神奇?!通過script+c/c++模塊的開發(fā)方式可以極大地增加手機(jī)應(yīng)用的靈活性(手機(jī)程序最困難的不是開發(fā),而是什么都做好了,如何安裝到用戶的手機(jī)里面去?!以及費(fèi)盡千辛萬苦安裝到用戶手機(jī)上了,如何升級(jí)?!一次升級(jí)哪怕只是ui改變一點(diǎn)點(diǎn),也可能流失用戶),偶希望通過這三篇文章加上這些測(cè)試代碼,能夠起到拋磚引玉的作用(當(dāng)然文章中的不足之處引來各位高手的更多的板磚也是十分歡迎的),能夠?qū)Υ蠹矣行┰S用處。
三、實(shí)驗(yàn)結(jié)果展示
到了本文的最后,自然又進(jìn)入了“有圖有真相”時(shí)間,偶也可以給大家展示一下實(shí)驗(yàn)結(jié)果。
1、在2.0模擬器上面的運(yùn)行效果
2、在偶的nexus one上面的運(yùn)行效果
3、java調(diào)用v8wrapper的源代碼
| 文件: | V8Test.tar.gz | 大小: | 547KB | 下載: | 下載 | |
4、v8wrapper的源代碼
| 文件: | v8wrapper.tar.gz | 大小: | 3KB | 下載: | 下載 | |
5、完成后的apk安裝包
注意:v8data目錄下面的數(shù)據(jù)需要下載上面的"3、java調(diào)用v8wrapper的源代碼"才能得到。
| 文件: | v8test-signed.tar.gz | 大小: | 498KB | 下載: | 下載 | |
最后,通過偶的實(shí)驗(yàn),證明v8引擎————是馬。