論文第5章:Android繪圖平臺的實現

張雲貴發表於2013-07-31

面向移動裝置的向量繪圖平臺設計與實現

Design and Implementation of Mobile Device-oriented Vector Drawing Platform

引用本論文: 張雲貴. 面向移動裝置的向量繪圖平臺設計與實現[D]. 北京:北京理工大學軟體學院, 2013.

本論文的相似度為0%,是源創論文。歡迎評閱討論,請勿抄襲,如需更多資料請在部落格留言。

如果在研究或論文中使用到,歡迎回復或私信你的學校、姓名、研究領域,並在論文中新增引用或致謝。感謝你對開放成果的尊重和鼓勵。

 

第5章 Android繪圖平臺的實現

本章闡述了Android繪圖平臺的實現方法,主要是在跨平臺核心的基礎上實現Android畫布介面卡和檢視介面卡,對圖形顯示優化技術進行了實驗研究。

5.1 開發環境

5.1.1 SWIG的工作原理分析

如圖5‑1所示,藉助於SWIG實現Android程式的Java程式碼通過JNI訪問C++的類。在編譯階段SWIG工具從C++生成JNI的Java類檔案和相應的C++實現檔案,該實現檔案與原有的C++實現檔案一起通過NDK編譯為本地動態庫。

51_thumb5

圖5‑1 Android程式呼叫C++類的原理

如圖5‑2所示,利用SWIG的Director特性,指定某個具有虛擬函式的C++類可重定位,然後再生成C++匯出函式檔案和JNI的Java類檔案。在應用層中從對應的JNI類繼承並實現其函式,在執行該C++類的虛擬函式時對應的Android函式就被執行。

52_thumb2

圖5‑2 Android類從C++類的虛擬函式過載的原理

圖5‑2中,SwigDirector_SomeClass從C++的SomeClass派生,Android類從JNI的SomeClass類派生,在SwigDirector_SomeClass中通過呼叫JNIEnv類的CallStaticVoidMethod等函式實現在C++中呼叫Java的類函式,這樣Android類中相應的過載函式便得到呼叫,實現使用Android SDK的Java類來擴充套件C++類。

5.1.2 SWIG的執行效能分析

TouchVG平臺使用SWIG實現Android程式的Java類與核心的C++類之間的雙向呼叫,即Java類通過JNI呼叫C++類、C++類利用Director特性回撥Java類。

本文對這兩種呼叫方式進行評測,結果見圖5‑3。

53_thumb4

圖5‑3 SWIG在Android中的效能評測結果

圖5‑3包含下列四個評測專案:

(1)回撥繪圖:Android程式通過JNI呼叫跨平臺核心的一個測試函式,在該測試函式中多次回撥畫布介面卡的繪製直線段的函式,在該繪製函式中使用android.graphics包繪圖。其效能影響因素有JNI呼叫、回撥和繪圖。

(2)回撥不繪圖:與上一專案的差別是將畫布介面卡的繪製函式改為空實現,不受圖形庫的影響。其效能影響因素有JNI呼叫和回撥。

(3)直接繪圖:Android程式直接呼叫繪製直線段的函式,與JNI無關。

(4)正向呼叫:Android程式通過JNI多次呼叫跨平臺核心的一個測試函式。其效能影響因素是JNI呼叫,與JNI回撥及繪圖無關。

評測結果表明,基於虛擬函式重定位技術的回撥方式的效能與普通的JNI呼叫方式的差別較小,SWIG所增加的封裝函式並不會使繪圖效能明顯下降。

5.1.3 開發方式

Android繪圖平臺的實現方式如圖5‑4所示,編譯得到的繪圖平臺JAR包和核心本地動態庫可供應用程式使用。藉助於SWIG的Director機制,使用Android SDK實現了畫布介面卡和檢視介面卡,實現對核心功能的擴充套件。

將跨平臺核心使用NDK編譯到本地動態連結庫中,介面形式為JNI和封裝類庫。採用SWIG將C++類轉換為JNI的Java類,SWIG所生成的C++匯出函式檔案與跨平臺核心的程式碼檔案一起編譯為動態庫。編譯過程中使用Python指令碼自動修正SWIG所生成的程式碼中的缺陷,並自動將包含中文字元的檔案由UTF8臨時轉換為GBK編碼以便正常編譯轉換。

54_thumb2

圖5‑4 Android繪圖平臺的實現方式

對於Android本地動態連結庫的除錯定位難題,利用NDK提供的日誌輸出C函式和庫檔案,通過輸出日誌文字的方式來解決。用該方法診斷出了SWIG引起的JNI記憶體問題所在位置,最終採用Python指令碼自動修正SWIG所生成的檔案缺陷。

5.1.4 開發工具

使用了下列工具分別在Mac OS X 10.7和Windows 7上開發Android繪圖平臺:

(1)Android開發包(the ADT Bundle)r21.1,可以在Eclipse中除錯本地動態庫。

(2)Android NDK r8e,用於開發本地動態庫。

(3)SWIG 2.0.10,用於從C++標頭檔案生成JNI的類檔案和C++封裝檔案。

(4)Python 2.7,用於執行Python指令碼自動修正SWIG所生成的檔案缺陷。

(5)MSYS(Minimalist GNU for Windows)1.0,用於在Windows上模擬UNIX環境,執行Shell編譯指令碼。

5.1.5 SWIG編譯配置

在touchvg.swig檔案[1]中配置SWIG編譯選項,主要配置內容如下:

(1)在檔案前面定義下面兩個巨集:

SWIG_JAVA_NO_DETACH_CURRENT_THREAD

SWIG_JAVA_ATTACH_CURRENT_THREAD_AS_DAEMON

定義前者以便在每次呼叫原生程式碼後不與當前執行緒斷開(使用SWIG的Director機制後,在本地函式呼叫結束時一些JNI物件還需要繼續有效,不能與當前執行緒斷開)。定義後者將JNI環境附加在守護執行緒上(預設是附加在介面主執行緒上,在Activity退出時可能會崩潰)。

(2)輸出JNI_OnLoad函式。Dalvik虛擬機器要求必須實現JNI_OnLoad函式,本平臺僅簡單返回JNI_VERSION_1_6,由SWIG生成的程式碼自動註冊本地函式。

(3)指定GiCanvas和GiView需要生成重定向類,並匯出相應的標頭檔案。

(4)輸出要在Android程式碼中使用的核心介面。例如,GiCoreView類。

(5)新增TmpJOBJ輔助類,在解構函式中自動釋放JNI本地引用物件。SWIG生成的Director類中某些形參的本地引用物件沒有釋放,會因超出256個JNI引用物件的限制而溢位崩潰。例如,在GiCanvas的drawBitmap函式中,name字串物件所對應的本地引用物件“jstring jname”在呼叫了NewStringUTF函式後沒有呼叫DeleteLocalRef函式。

本文針對該問題提出的解決方法:將SWIG所生成的封裝檔案中的“jstring jname = 0”替換為“jstring jname = 0; TmpJOBJ jtmp(jenv, &jname)”,通過TmpJOBJ的析構作用自動呼叫DeleteLocalRef函式釋放引用。使用Python指令碼[2]自動進行替換SWIG生成的封裝檔案中的這類問題。

編寫了Shell指令碼(mk/swig.sh),用於執行SWIG工具生成JNI匯出函式的封裝檔案(touchvg_java_wrap.cpp)和JNI類檔案。JNI類的包名為touchvg.jni,其檔案輸出到工程的src/touchvg/jni目錄下,將與檢視介面卡的程式碼(src/touchvg/view目錄)共同生成為一個JAR檔案。

5.1.6 NDK編譯配置

Android繪圖平臺的程式碼目錄結構見第20頁的圖3‑7。在工程的jni/Android.mk檔案[3]中配置本地動態庫的NDK編譯選項,主要有:

(1)基於絕對地址$(LOCAL_PATH)/../../../core/include在LOCAL_C_INCLUDES中指定核心的標頭檔案路徑,基於相對地址 ../../../core/src 在LOCAL_SRC_FILES中指定跨平臺核心的實現檔案(*.cpp,使用絕對的路徑無法編譯)。

(2)因為SWIG的Director程式碼使用了RTTI執行時型別資訊,所以在LOCAL_CFLAGS中指定-frtti選項。

(3)為了使用STL,在jni/Application.mk中指定“APP_STL := stlport_static”。

本文編寫了Shell指令碼(ndk.sh),在其中進入android\demo\jni目錄自動執行ndk-build編譯出本地動態連結庫libtouchvg.so,在編譯過程中自動應用Android.mk中的配置資訊。Onur Cinar[4]介紹了在Android.mk中包含指令碼的方法,可自動執行指令碼。

本文在多個平臺編譯時發現Shell指令碼檔案應使用Unix行結束符(LF),不能是DOS結束符或Mac結束符,儘可能避免使用中文字元。

5.2 基於Android Canvas實現畫布介面卡

在第12頁的2.2.3節介紹了Android二維繪圖主要涉及的框架。本文主要基於兩種檢視類設計繪圖檢視類:android.view.View和android.view.SurfaceView,在繪圖檢視類中使用Android Canvas畫布類(使用android.graphics包)渲染。

5.2.1 畫布原語與Android Canvas的對映

本文基於android.graphics包設計畫布介面卡類touchvg.view.CanvasAdapter,該類實現touchvg.jni.GiCanvas中的畫布原語函式,後者是通過SWIG從跨平臺核心的GiCanvas介面自動生成的。在核心中呼叫畫布介面GiCanvas的函式時,畫布介面卡將被回撥執行,從而允許使用Android Canvas渲染。

畫布介面卡主要使用了android.graphics包中這些類:Canvas畫布類訪問繪圖函式介面,Paint類指定顏色等繪圖屬性,Path類構建路徑,Bitmap指定點陣圖資料。在繪圖檢視的onDraw函式中將Canvas畫布物件傳入畫布介面卡,後續繪圖將在該畫布物件上進行。在離屏點陣圖上渲染時,從點陣圖構建畫布物件,接著傳入畫布介面卡。

因為Paint物件只能指定一個顏色,無法區分畫筆顏色和畫刷顏色,所以畫布介面卡針對畫筆、畫刷和文字顯示分別使用一個Paint物件:mPen、mBrush、mTextPen。以顯示一個紅邊藍底的橢圓為例,先設定mPen的顏色為紅色、mBrush的顏色為藍色,然後分別使用mPen和mBrush作為引數繪製橢圓。為了讓文字顏色和圖形顏色同步,在setPen函式中同時設定畫筆mPen和文字屬性mTextPen的顏色。

這三種Paint物件的引數設定見表5‑1。

表5‑1 Paint物件的引數設定

畫筆

畫刷

文字

mPen.setAntiAlias(true)

 

mTextPen.setAntiAlias(true)

mPen.setDither(true)

 

mTextPen.setDither(true)

mPen.setStyle(STROKE)

mBrush.setStyle(FILL)

 

mPen.setPathEffect(null)

mBrush.setColor(0)

 

mPen.setStrokeCap(Cap.ROUND)

   

mPen.setStrokeJoin(Join.ROUND)

   

表5‑1中,畫刷預設填充顏色為透明色,即不填充。畫筆的預設線型為實線,線端為圓端,這樣在繪製短線時更像一個圓點。為了讓點線等虛線型別的空白間隙整齊,在setPen函式中對所有虛線型別設定平端的線端型別。

與iOS繪圖平臺的實現類似,Android畫布介面卡按表5‑2所示的對映方法實現了畫布原語函式。由跨平臺核心中的TestCanvas類生成和顯示向量圖形和影象。

在實現這些函式時,本文對下列內容進行了特殊處理或總結。

(1)在View中呼叫畫布介面卡的clearRect函式,無法使指定區域透明,如圖5‑5(i)所示。只能在原有圖形基礎上填充顏色,指定透明色將填充為黑色。在SurfaceView中呼叫畫布介面卡的clearRect函式,可以擦除指定區域內的圖形,變為透明區域,如圖5‑5(k)所示。

(2)當程式和檢視使用了硬體加速特性後,呼叫clipPath會崩潰,其原因是在硬體加速時不支援clipPath函式。解決方法是在UnsupportedOperationException異常出現後將對應的檢視的層型別設定為軟體實現方式(LAYER_TYPE_SOFTWARE)。

(3)在SurfaceView檢視中使用渲染執行緒連續繪圖能夠達到48~56FPS的更新速度,實驗效果如圖5‑5(n)所示。測試用例為繪製不斷延長的三次貝塞爾曲線,測試條件為MOTO MZ606平板電腦(Android 4.0.3,1280×800)。

表5‑2 畫布原語與android.graphics的對映

畫布原語

測試號

Android函式對應關係

clearRect

i k

mCanvas.drawColor(mBkColor, Mode.CLEAR),需要設定剪裁區域

drawRect

a

mCanvas.drawRect,使用mPen和mBrush

drawEllipse

b

mCanvas.drawOval,寬高不超過1時使用drawPoint

beginPath

多個

建立路徑物件mPath

moveTo

多個

mPath.moveTo

lineTo

c

mPath.lineTo

bezierTo

e

mPath.cubicTo

quadTo

f

mPath.quadTo

closePath

c

mPath.close

drawPath

多個

mCanvas.drawPath(mPath, mBrush)、.drawPath(mPath, mPen)

drawHandle

g

mCanvas.drawBitmap,在指定點顯示

drawBitmap

g

mCanvas.drawBitmap,指定Matrix矩陣變換物件

drawTextAt

h

mTextPen.setTextSize、mCanvas.drawText,用到FontMetrics

setPen

多個

mPen.setColor、mPen.setStrokeWidth、mTextPen.setColor

mPen.setPathEffect、mPen.setStrokeCap

setBrush

多個

mBrush.setColor

saveClip

m

mCanvas.save(CLIP_SAVE_FLAG)

restoreClip

m

mCanvas.restore()

clipRect

m

mCanvas.clipRect

clipPath

m

mCanvas.clipPath,硬體加速時需要將檢視的層改為軟體實現型別

drawLine

d

mCanvas.drawLine

注:其中的測試號為圖5‑5中的測試子圖號。

55_thumb4

圖5‑5 Android畫布適配效果

5.2.2 影象的顯示和管理

在繪圖檢視中管理影象物件,畫布介面卡從檢視獲取影象。顯示介面函式為:

void drawBitmap(String name, float xc, float yc, float w, float h, float angle)

其中,使用名稱name標識影象物件,(xc,yc)為影象的中心顯示位置,w和h為顯示目標寬高,angle為旋轉的角度(世界座標系中的逆時針方向)。

影象繪製的基點在影象的左上角,畫布的座標系為ULO型別,繪製過程為:

(1)根據name從繪圖檢視獲取Bitmap物件;

(2)計算變換矩陣:將顯示基點由影象的左上角平移到中心,反向旋轉angle角度(弧度轉換為度),將寬高分別放縮到w和h,最後平移到(xc,yc)。

(3)使用此矩陣顯示影象物件。

本文實驗發現在顯示大圖片時,載入圖片所需時間遠大於顯示影象的時間,因此減少圖片載入次數能加快顯示速度。本文采用下面兩種方法進行影象管理:

(1)在繪圖檢視類中使用LruCache快取圖片。定義LruCache<String, Bitmap>型別的成員變數,以影象標識串(drawBitmap中的name)為鍵值管理影象物件。

(2)載入圖片前先檢查圖片的寬高,如果太大就以降低取樣率方式載入圖片。

5.3 繪圖檢視的設計和實驗

為了提高檢視的顯示質量和效能,本文針對View、SurfaceView進行了實驗。

5.3.1 實現方式

繪圖檢視使用畫布介面卡CanvasAdapter繪圖,由跨平臺核心中的GiCoreView和TestCanvas類自動顯示測試圖形。繪圖檢視類的關係見圖5‑6,實現方式說明如下。

(1)GraphView。從View派生,在onDraw中繪圖,呼叫invalidate()重繪。在onDraw中呼叫核心檢視的drawAll函式顯示所有圖形。在觸控響應函式中呼叫核心檢視的onGesture函式傳遞手勢動作,由後者在某個互動命令中呼叫檢視的redraw等函式,這將回撥到GraphView的檢視介面卡(ViewAdapter),後者呼叫檢視的invalidate()標記需要重繪。

(2)皮膚表面檢視。從SurfaceView派生。呼叫setZOrderOnTop(true)設定為皮膚視窗,顯示於宿主視窗之上。呼叫getHolder().setFormat(TRANSPARENT)設定其Surface背景透明,以顯示宿主視窗的內容。在Surface就緒和重新整理顯示時啟動渲染執行緒,在繪圖執行緒中獲取畫布繪圖,由核心檢視的drawAll函式顯示所有圖形。

(3)媒體表面檢視。從SurfaceView派生。預設就是媒體視窗,顯示於宿主視窗之下,自動在宿主視窗上設定透明區域以便讓SurfaceView上的內容可見。在Surface就緒和重新整理顯示時啟動渲染執行緒,在繪圖執行緒中獲取畫布繪圖。

(4)GraphViewCached。從View派生,使用一個點陣圖快取圖形內容,在onDraw函式中顯示該點陣圖。

核心呼叫regenAll函式時銷燬該點陣圖,下次onDraw函式執行時重新生成點陣圖。應用增量繪圖技術,新增新圖形後呼叫regenAppend函式,直接在該點陣圖上繪製新圖形,下次onDraw函式執行時顯示有新內容的點陣圖。

56_thumb2

圖5‑6 Android渲染檢視的類關係

(5)靜態View + 動態View。在佈局檢視中建立兩個基於View的檢視類(GraphView和DynDrawStdView),分別顯示不變的圖形和經常改變的內容,前者渲染的內容通常較多。

(6)皮膚表面檢視 + View。在佈局檢視中建立GraphSfView和DynDrawStdView檢視。在GraphSfView中顯示靜態圖形,GraphSfView位於視窗頂端。在DynDrawStdView中顯示動態圖形。

(7)靜態View + 皮膚表面檢視。在佈局檢視中建立GraphView和DynDrawSfView檢視。在GraphView中顯示靜態圖形,在DynDrawSfView中顯示動態圖形。DynDrawSfView是皮膚表面檢視,在子執行緒中獲取畫布繪圖,每次重新整理顯示時啟動渲染執行緒。

(8)皮膚表面檢視 + 皮膚表面檢視。在兩個位於視窗頂端的檢視類中分別顯示靜態圖形和動態圖形。

(9)媒體表面檢視 + View。在GraphSfView中顯示靜態圖形,GraphSfView位於根檢視層次的底端,在DynDrawStdView中顯示動態圖形。

(10)媒體表面檢視 + 皮膚表面檢視。在兩個基於SurfaceView的檢視類中分別渲染靜態圖形和動態圖形,靜態圖形在視窗底端渲染,動態圖形在頂端渲染。

以上的檢視類按表5‑3呼叫核心檢視的顯示函式,由GiCoreView顯示圖形。

表5‑3 Android檢視類與核心顯示函式的對應關係

檢視類

對應的GiCoreView顯示函式

檢視類

GiCoreView函式

GraphView

drawAll

DynDrawStdView

dynDraw

GraphSfView

drawAll

DynDrawSfView

dynDraw

GraphViewCached

drawAppend、dynDraw、drawAll

   
5.3.2 實驗結果

本文針對View、SurfaceView進行了上述十組實驗,實驗結果見圖5‑7和表5‑4。實驗條件為:Android 3.0、模擬器(320×480),其中使用較小解析度是便於在本文中插入螢幕截圖。在MOTO MZ606平板電腦(Android 4.0.3,1280×800)上實驗後也得出相同的結論。

從這十組實驗得到下列結論:

(1)普通的繪圖方式基於View實現定製檢視,在onDraw函式中使用Canvas進行繪圖。該方式使用簡單,適合繪製簡單圖形。缺點是繪圖速度較慢,重新整理一個檢視會使同級的其他檢視被動重新整理,容易引起顯示效能下降問題。

(2)互動式繪圖顯示速度快的方式有:使用增量繪圖技術的普通檢視(GraphViewCached);在SurfaceView中繪製動態圖形的雙層繪圖檢視。使用增量繪圖技術的優點是可以只需要一個繪圖檢視,雙層繪圖檢視的優點是可以在子執行緒中繪圖,能提高重新整理幀率。可以將兩者的優點結合起來,在GraphViewCached中顯示靜態圖形,在SurfaceView中繪製動態圖形。

(3)如果要在SurfaceView中非同步繪製靜態圖形,合適的使用條件有:a、與其他內容檢視沒有重疊區域;b、在視窗頂端透明顯示,不要在此區域顯示按鈕等臨時介面控制元件;c、在視窗底端顯示,視窗裡沒有不透明的大面積介面元素。

57_thumb2

圖5‑7 Android渲染檢視效果

表5‑4 Android渲染檢視的組合實驗情況

圖號

檢視搭配型別

顯示速度和問題

a

GraphView

b

GraphSfView(皮膚表面檢視)

慢,圖形遮擋按鈕

c

GraphSfView(媒體表面檢視)

d

GraphViewCached

動態和靜態繪圖都很快

e

靜態View + 動態View

慢,另一檢視被動重新整理

f

皮膚表面檢視 + View

快,靜態圖形遮擋按鈕和動態圖形

g

靜態View + 皮膚表面檢視

h

皮膚表面檢視 + 皮膚表面檢視

快,動態繪圖拖尾明顯,遮擋按鈕

i

媒體表面檢視 + View

快,不透明檢視會遮擋靜態圖形

-

媒體表面檢視 + 皮膚表面檢視

快,不透明檢視會遮擋靜態圖形

注:其中的圖號為圖5‑7中的子圖號。

5.4 Android繪圖平臺的結構

5.4.1 靜態結構

根據繪圖檢視的實驗結果,Android繪圖平臺按圖5‑8設計靜態類結構(省略了跨平臺核心的內部結構和SWIG的Director類),相應類的說明見表5‑5。

58_thumb6

圖5‑8 Android繪圖適配模組的結構

表5‑5 Android繪圖適配模組的類

含義和職責

GraphViewHelper

面向應用程式的繪圖封裝介面類,提供常用API

GraphViewCached

顯示靜態圖形的檢視類,使用了基於快取點陣圖的增量繪圖技術,負責觸控手勢識別,委託核心的GiCoreView實現圖形顯示和手勢操作

DynDrawSfView

顯示動態圖形的SurfaceView檢視類,委託GiCoreView顯示動態圖形

ViewAdapter

檢視介面卡,允許核心回撥Android檢視,通知重新整理顯示

CanvasAdapter

使用Android Canvas實現的畫布介面卡

GiCoreView

跨平臺核心的檢視分發器,託管圖形物件,分發顯示請求和手勢資訊給圖形列表和當前命令

應用程式使用繪圖檢視有兩種方式:(1)僅使用GraphViewCached檢視,適合圖形量不太多的場合。(2)通過GraphViewHelper建立一個佈局檢視,自動建立GraphViewCached和DynDrawSfView檢視,適合動態繪圖幀率要求較高的場合。

5.4.2 應用效果

在Android繪圖平臺(屬於TouchVG框架)中應用多層繪圖技術分離靜態圖形檢視和動態圖形檢視,提高了動態互動式繪圖的回顯速度。在靜態圖形檢視中應用增量繪圖技術,在連續繪製曲線圖形時沒有明顯的拖尾現象。因此,繪圖體驗較流暢。

在跨裝置平臺的核心中使用繪圖命令可以顯示各種圖形,在核心檢視中使用仿射變換可以實現放縮顯示。圖5‑9展示了在不同Android版本的模擬器和平板電腦上的實際繪圖效果。

59_thumb2

圖5‑9 Android綜合繪圖效果

5.5 本章小結

本章詳細描述了SWIG在Android中的應用方法和擴充套件機制,針對出現的本地引用問題提出了修正方法。實驗表明,SWIG所增加的封裝函式並不會使繪圖效能明顯下降。描述了基於Android Canvas實現畫布介面卡的方式,實現了圖形和影象的向量化顯示。畫布介面卡的單元測試使用了跨平臺核心自動繪製圖形,證明在核心中可以使用C++在Android上互動式繪圖。

對普通檢視、皮膚表面檢視和媒體視窗進行了組合實驗,總結出互動式繪圖顯示速度快、不出現遮擋問題的兩種方式:使用增量繪圖技術的單一檢視方式;在SurfaceView中繪製動態圖形的雙層檢視方式。

最後給出了Android繪圖平臺的設計結構和應用效果。


[1] 詳細的SWIG編譯選項見檔案:https://raw.github.com/rhcad/vglite/master/android/demo/jni/touchvg.swig 。

[2] Python指令碼見檔案:https://raw.github.com/rhcad/vglite/master/android/demo/jni/replacejstr.py 。

[3] NDK編譯配置檔案見:https://raw.github.com/rhcad/vglite/master/android/demo/jni/Android.mk 。

[4] Onur Cinar. Pro Android C++ with the NDK. Berkeley: Apress, 2012

相關文章