Android SurfaceView的繪制詳解



 在Android系統中,有一種特殊的視圖,稱為SurfaceView,它擁有獨立的繪圖表面,即它不與其宿主窗口共享同一個繪圖表面。由於擁有獨立的繪圖表面,因此SurfaceView的UI就可以在一個獨立的線程中進行繪制。又由於不會占用主線程資源,SurfaceView一方面可以實現復雜而高效的UI,另一方面又不會導致用戶輸入得不到及時響應。

SurfaceView也是繼承View的,但是跟其他TextView,ImageView等就不一樣;最大的不一樣,SurfaceView並沒有使用View的宿主窗口(下面我都管這個宿主窗口為頂級surface),它有自己獨立的surface,繪制是在這個獨立的surface上繪制。

而TextView,ImageView等視圖控件同樣也是繼承View的子類,他們卻是共用的一個surface,這個surface在ViewRootImpl創建,以lockCanvas()的形式返回canvas去管理繪制傳遞下去。

先從一個簡單的demo講起:

public class MySurfaceView extends SurfaceView implements Runnable, SurfaceHolder.Callback {
private SurfaceHolder mHolder; // 用於控制SurfaceView
private Thread t; // 聲明一條線程
private boolean flag; // 線程運行的標識,用於控制線程
private Canvas mCanvas; // 聲明一張畫布
private Paint p; // 聲明一支畫筆
private int x = 50, y = 50, r = 10; // 圓的坐標和半徑
public MySurfaceView(Context context) {
super(context);
mHolder = getHolder(); // 獲得SurfaceHolder對象
mHolder.addCallback(this); // 為SurfaceView添加狀態監聽
p = new Paint(); // 創建一個畫筆對象
p.setColor(Color.WHITE); // 設置畫筆的顏色為白色
setFocusable(true); // 設置焦點
}
/**
* 自定義一個方法,在畫布上畫一個圓
*/
public void doDraw() {
mCanvas = mHolder.lockCanvas(); // 獲得畫布對象,開始對畫布畫畫
mCanvas.drawRGB(0, 0, 0); // 把畫布填充為黑色
mCanvas.drawCircle(x, y, r, p); // 畫一個圓
mHolder.unlockCanvasAndPost(mCanvas); // 完成畫畫,把畫布顯示在屏幕上
}
/**
* 當SurfaceView創建的時候,調用此函數
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
t = new Thread(this); // 創建一個線程對象
flag = true; // 把線程運行的標識設置成true
t.start(); // 啟動線程
}
/**
* 當SurfaceView的視圖發生改變的時候,調用此函數
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
/**
* 當SurfaceView銷毀的時候,調用此函數
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
flag = false; // 把線程運行的標識設置成false
}
/**
* 當屏幕被觸摸時調用
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
x = (int) event.getX(); // 獲得屏幕被觸摸時對應的X軸坐標
y = (int) event.getY(); // 獲得屏幕被觸摸時對應的Y軸坐標
return true;
}
/**
* 當用戶按鍵時調用
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if(keyCode == KeyEvent.KEYCODE_DPAD_UP){ //當用戶點擊↑鍵時
y--; //設置Y軸坐標減1
}
return super.onKeyDown(keyCode, event);
}
@Override
public void run() {
while (flag) {
doDraw(); // 調用自定義畫畫方法
try {
Thread.sleep(2000); // 讓線程休息50毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

MySurfaceView的要點:

1,繼承SurfaceView父類和SurfaceHolder.Callback接口,並實現surfaceCreated(SurfaceHolder holder),surfaceChanged,surfaceDestroyed方法來管理surfaceview的生命周期。

2,構造方法中通過SurfaceHolder來設置this為callback。

mHolder = getHolder(); // 獲得SurfaceHolder對象 
mHolder.addCallback(this); // 為SurfaceView添加狀態監聽
3,進行具體的主體繪制時需主動通過lockCanvas(Rect dirty)獲取canvas,這也是跟View不同的地方。

注意:

1,這里不知道你有沒有注意到,我們在MySurfaceView的繪制里面開了個子線程。學過android肯定知道UI線程一定在主線程,而SurfaceView的繪制可以在子線程實現,這也正是SurfaceView的一大亮點。如此可以把一些耗時的渲染讓在SurfaceView來繪制,就不會導致主線程阻塞。

可以使用子線程的原因(個人理解):android的主UI是在ViewRootImpl提供的surface,作為頂級surface是要一直呈現給用戶的,所有的View都是在頂級Surface進行繪制的。而SurfaceView擁有自己獨立的surface,由WindowManagerService完成繪制,並以在頂級surface上打洞的形式來呈現自己。

因為SurfaceView擁有獨立的surface,最好的實現應該是要放在獨立的線程來繪制。


這里有幾個類要詳細講一下:Surface,SurfaceHolder ,SurfaceHolder.Callback

Surface

surface對應着一個layer,是原始圖像緩沖區(raw buffer)的一個句柄,而原始圖像緩沖區是由屏幕圖像合成器(screen compositor)管理的。我們可以理解surface就是一段最終用來顯示的內存。每個SurfaceView都有一個獨立的surface。

而View以及一般View的子類並不持有獨立的surface,他們持有的是一個頂級surface通過lockCanvas(Rect dirty)指定的部分區域,並通過canvas來繪制這部分區域。這個頂級surface也對應着一個layer。SurfaceView的surface是通過在頂級surface下打洞的形式來呈現自己。

最終,這些surface會被surfaceFlinger來合成並最終顯示出來。

Surface是實現了Parcelable接口的,所以可以實現跨進程傳輸surface。事實上,surface的刷新都在updateWindow()中具體實現。而updateWindow()中正式跨進程的調用了WindowManagerService來進行surface的重繪並把新的surface返回SurfaceView。

IWindowSession mSession;

SurfaceView->updateWindow():

 relayoutResult = mSession.relayout(
mWindow, mWindow.mSeq, mLayout, mWidth, mHeight,
visible ? VISIBLE : GONE,
WindowManagerGlobal.RELAYOUT_DEFER_SURFACE_DESTROY,
mWinFrame, mOverscanInsets, mContentInsets,
mVisibleInsets, mStableInsets, mConfiguration, mNewSurface);

IWindowSession.aidl

 int relayout(IWindow window, int seq, in WindowManager.LayoutParams attrs,
int requestedWidth, int requestedHeight, int viewVisibility,
int flags, out Rect outFrame, out Rect outOverscanInsets,
out Rect outContentInsets, out Rect outVisibleInsets,
out Configuration outConfig, out Surface outSurface);

這里希望讀者可以區分aidl的標識符in和out的區別。in代表是輸入,out對應的字段是要輸出的,這里跟c語言里指針的用法類似。這里的outSurface看起來像是輸入,其實最終是作為一個輸出來表示新的surface:mNewSurface.

在SurfaceView中輸入的參數mWindow,mWindow.mSeq,mLayout等,輸出的參數有outFrame,outConfig,outSurface等。

很明顯,這里是把SurfaceView中的一些LayoutParams布局屬性值傳遞給了mSession,而mSession又調用WindowManagerService的relayoutWindow()來完成重繪過程,並將新的outSurface返回給SurfaceView的mNewSurface。

mSurface.transferFrom(mNewSurface);
在updateWindow()中通過transferFrom()即可把新的mNewSurface賦值給mSurface。

SurfaceHolder

SurfaceHolder的主要作用:

1,getSurface(),對surface的持有和管理

2,lockCanvas(),unlockCanvasAndPost(),對canvas的創建和釋放

3,addCallback(),removeCallback(),對實現SurfaceHolder.Callback的MySurfaceView子類注冊管理。

4,通過調用surfaceholder的setFixedSize、setSizeFromLayout、setFormat都會觸發重繪surface得到新的surface。

注意:

1,這里的lockCanvas()和lockCanvas(Rect dirty)的區別,dirty指向一個指定的重繪區域,區域之外部分不重繪,如此可提高繪制效率。


SurfaceHolder.Callback

SurfaceHolder.Callback管理Surface的生命周期,並監聽surface的變化。每當surface狀態發生變化的時候會以各種形式通知SurfaceView,並最終都會觸發updateWindow(),進而觸發了callback的三個方法,並把新的surfaceholder輸出。

SurfaceHolder.Callback具有如下的接口:
surfaceCreated(SurfaceHolder holder):

當Surface第一次創建后會立即調用該函數。程序可以在該函數中做些和繪制界面相關的初始化工作,一般情況下都是在另外的線程來繪制界面,所以不要在這個函數中繪制Surface。 

surfaceChanged(SurfaceHolder holder, int format, int width,int height):

當Surface的狀態(大小和格式)發生變化的時候會調用該函數,在surfaceCreated調用后該函數至少會被調用一次。 

surfaceDestroyed(SurfaceHolder holder):

當Surface被摧毀前會調用該函數,該函數被調用后就不能繼續使用Surface了,一般在該函數中來清理使用的資源。 


SurfaceView和View的比較

1,SurfaceView和View最大的區別在於,surfaceView是在一個新起的單獨線程中可以重新繪制畫面而View必須在UI的主線程中更新畫面。

2,View是通過ViewRootImpl提供的頂級surface進行lockCanvas(Rect dirty),返回的canvas會在指定的dirty范圍進行繪制。

而每一個SurfaceView擁有獨立的surface,通過在頂級surface上打洞來顯示自己。這些surface對應底層的Layer,由SurfaceFlinger根據這些layer的內容以及層級進行混合並最終顯示。

The surface is Z ordered so that it is behind the window holding its SurfaceView; the SurfaceView punches a hole in its window to allow its surface to be displayed. 

3,在使用上SurfaceView需通過SurfaceHolder.lockCanvas()主動獲取canvas。而View是直接由頂級surface在ViewRootImple就已經lockCanvas后,把canvas傳遞到View的draw(canvas)。

4,因為View是只擁有一個頂級surface,子view都是共用一個surface,所以在繪制時performTraversals()是對一個樹結構的view群進行測量、布局、繪制的遍歷。

而一個SurfaceView擁有一個surface,它只需要對自己進行測量、布局和繪制。流程簡單的多。

5,SurfaceView實現重繪的方式更直觀,每次主動調用doDraw()(demo中我自定義的方法)就是重新繪制。而View因為不能直接通過surface.lockCanvas獲取canvas,只能通過調用invalidate()去觸發父視圖ViewRootImpl去調用performTraversal()去實現重繪。


SurfaceView和View的使用場景

1 ,被動更新畫面的。比如棋類,這種用view就好了。因為畫面的更新是依賴於 onTouch 來更新,可以直接使用 invalidate。 因為這種情況下,這一次Touch和下一次的Touch需要的時間比較長些,不會產生影響。

2 ,主動更新。比如一個人在一直跑動。這就需要一個單獨的thread不停的重繪人的狀態,避免阻塞main UI thread。所以顯然view不合適,需要surfaceView來控制。


Surface和Canvas的區別

surface就可以這樣理解:它是內存中一塊區域,它是surfaceview可見的那個部分,繪圖操作作用於它,然后它就會被顯卡之類的顯示控制器繪制到屏幕上。大家都知道,內存區的對象是有生命周期的,可以動態的申請創建和銷毀,當然也可能會更新。於是,就有了作用於這個內存區的操作,這些操作就是surfaceCreated/Changed/Destroyed。三個操作放在一起,就是SurfaceHolder.Callback。

Canvas倒像是一個擁有一段繪制區域的畫家,通過surface.lockCanvas(Rect dirty)來獲得,並指定canvas只能在dirty范圍進行對surface指定區域的繪制。

关注微信公众号

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com