大部分 Android 應用程式,應該都是用 Java Code 完成所有的工作,包括繪圖。但,有些情況下,你會希望繪圖有更快的反應;例如 game ,這時 native code 可能是一個選擇。
在 Android 上,有一個 graphic engine ,稱為 Skia 。 Skia 的功能約等於 Cairo ,功能上相似,但 Skia 的支援沒 Cairo 廣。其實, Android 的 Java Code 都是透過 Skia 進行繪圖,而 Skia 主要的 class type 是 SkCanvas ,所的有繪圖功能都建構在這個 class 上。因此,如果我們能在 native code 取得 Android 所建立的 SkCanvas ,能直接使用 Skia 對畫面做輸出。
在 Android 的 UI 設計裡,每一個 UI component 都是一個 view ;例如: button 、 label 等等,全是 view 。當 Android 要畫一個 view 時,會呼叫 view 的 onDraw() 畫出 component 的外觀。而Android 會將一個 android.graphics.Canvas type 的物件,當成參數給 onDraw() 。 onDraw() 就在這個 canvas 上輸出 component 的外觀,例如畫一個 button 。這個 canvas 其實就對映到一個 SkCanvas ,我們只要在這個 canvas 上作畫,就等於畫到畫面上的一個區域。
Android 是透過 JNI 呼叫 native code ,於是我們在 Java side 定義了一個 View ,使該 View 透過 JNI 把 canvas 傳送給我們的 native code ,以便在 native code 裡繪圖。
001 package com.example; 002 import android.view.View; 003 import android.graphics.Canvas; 004 005 class myview extends View { 006 public void onDraw(Canvas canvas) { 007 nativedraw(canvas); 008 } 009 native void nativedraw(Canvas canvas); 010 static { 011 System.loadLibrary("mynative"); 012 } 013 }
在個這 class 裡,我們將 canvas 透過 JNI 傳給 native code ,接下來我們定義 JNI 的 native function 。
001 #include <SkCanvas.h> 002 003 extern "C" { 004 005 #include <jni.h> 006 007 void 008 Java_com_example_myview_nativedraw(JNIEnv *env, jobject thiz, jobject obj) { 009 jclass cls; 010 jfieldID fid; 011 SkCanvas *canvas; 012 013 cls = env->GetObjectClass(obj); 014 fid = env->GetFieldID(cls, "mNativeCanvas", "I"); 015 canvas = (SkCanvas *)env->GetIntField(obj, fid); 016 /* ..... draw on canvas .... */ 017 } 018 019 }
obj 這個參數就是 Java side 的 canvas ,而 SkCanvas 是放在 Java side canvas 的 mNativeCanvas 欄位,是一個整數。這個整數其實就是 SkCanvas 的位址,我們直接 casting 成 SkCanvas 的 pointer ,如此我們就取得 SkCanvas ,可以透過 Skia 介面直接作圖。
注意!! JNI 是定義在 C 語言上,而 Skia 是 C++ 的 library 。因此,我們使用 C++ 寫 native code ,但在 JNI 的 function 定義時,必需用 extern "C" 包起來,告知 C++ compiler 這個 function 是使用C 的呼叫規則。
如果你要在 native code 裡作動畫,那麼就必需要能更新畫面。如果只是把 canvas 的位址存起來,不斷的對 canvas 做更新,你會發覺畫面並不會跟著更新。正確的作法是要呼叫 View 的 invalidate() function ,讓 Android 進行更新的動作。
當 View.invalidate() 被呼叫之後, Android framework 會呼叫 View 的 onDraw(),這時呼叫 Native code 進行畫圖,更新的結果才會顯示在畫面上。因此,進行動畫時,Java side 必需定期的呼叫 View.invalidate() ,以進行畫面的更新。
SurfaceView 也是 View ,但有獨立的 buffer ,稱為 surface 。由於這個 buffer 不需透過 Androidframework 作畫面更新,能直接對應到畫面上的區塊,提供更好的效能。(其實還是間接更新到畫面上,但少了 Android framework 這一層,而且可透過 hardware 加速。加速的功能視平台而定。)
當你需要更好的效能時, 繼承 SurfaceView 也是一個方式。透過 SurfaceView 時, canvas 不再是透過 onDraw() 取得了。你能透過呼叫 SurfaceView.getHolder() 取得 surface holder ,而 SurfaceHolder.lockCanvas() 會傳回 SkCanvas 。 Native code 可以在這個 SkCanvas 上作畫,然後呼叫 SurfaceHolder.unlockCanvasAndPost() ,將內容更新到畫面上。
每次更新畫面前,都要透過 SurfaceHolder.lockCanvas() 取得人一個新的 SkCanvas,在這個 canvas 上作畫才行。作完後都必需呼叫 SurfaceHolder.unlockCanvasAndPost() 進行更新。如果才能正確的更新畫面。
另外, SurfaceHolder.lockCanvas() 傳回的 SkCanvas 並不會保存上次繪圖的內容。請特別注意。
SurfaceHolder.lockCanvas() 可以傳入一個參數,指定要更新的部分。透過指定特定區域,我只需重繪該部分,而不需更新整個畫面,並改善更新的效能。
會研究這個部分,是因為現在正在 porting MadButterfly 到 Android 上。在此順手記下一些心得。
原贴地址:http://www.codemud.net/~thinker/GinGin_CGI.py/show_id_doc/404