1. 程式人生 > >Android SurfaceView的繪製詳解

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講起:

[java] view plain copy print?
  1. publicclass MySurfaceView extends SurfaceView implements Runnable, SurfaceHolder.Callback {  
  2.    private SurfaceHolder mHolder; // 用於控制SurfaceView 
  3.    private Thread t; // 宣告一條執行緒 
  4.    privateboolean flag; // 執行緒執行的標識,用於控制執行緒 
  5.    private Canvas mCanvas; // 宣告一張畫布 
  6.    private Paint p; // 宣告一支畫筆 
  7.    privateint x = 50, y = 50, r = 10// 圓的座標和半徑 
  8.    public MySurfaceView(Context context) {   
  9.      super(context);   
  10.      mHolder = getHolder(); // 獲得SurfaceHolder物件 
  11.      mHolder.addCallback(this
    ); // 為SurfaceView新增狀態監聽 
  12.      p = new Paint(); // 建立一個畫筆物件 
  13.      p.setColor(Color.WHITE); // 設定畫筆的顏色為白色 
  14.      setFocusable(true); // 設定焦點 
  15.    }   
  16.    /**  
  17.    * 自定義一個方法,在畫布上畫一個圓  
  18.    */
  19.    publicvoid doDraw() {   
  20.      mCanvas = mHolder.lockCanvas(); // 獲得畫布物件,開始對畫布畫畫 
  21.      mCanvas.drawRGB(000); // 把畫布填充為黑色 
  22.      mCanvas.drawCircle(x, y, r, p); // 畫一個圓 
  23.      mHolder.unlockCanvasAndPost(mCanvas); // 完成畫畫,把畫布顯示在螢幕上 
  24.    }   
  25.    /**  
  26.    * 當SurfaceView建立的時候,呼叫此函式  
  27.    */
  28.    @Override
  29.    publicvoid surfaceCreated(SurfaceHolder holder) {   
  30.      t = new Thread(this); // 建立一個執行緒物件 
  31.      flag = true// 把執行緒執行的標識設定成true 
  32.      t.start(); // 啟動執行緒 
  33.    }   
  34.    /**  
  35.    * 當SurfaceView的檢視發生改變的時候,呼叫此函式  
  36.    */
  37.    @Override
  38.    publicvoid surfaceChanged(SurfaceHolder holder, int format, int width,   
  39.        int height) {   
  40.    }   
  41.    /**  
  42.    * 當SurfaceView銷燬的時候,呼叫此函式  
  43.    */
  44.    @Override
  45.    publicvoid surfaceDestroyed(SurfaceHolder holder) {   
  46.      flag = false// 把執行緒執行的標識設定成false 
  47.    }   
  48.    /**  
  49.    * 當螢幕被觸控時呼叫  
  50.    */
  51.    @Override
  52.    publicboolean onTouchEvent(MotionEvent event) {   
  53.      x = (int) event.getX(); // 獲得螢幕被觸控時對應的X軸座標 
  54.      y = (int) event.getY(); // 獲得螢幕被觸控時對應的Y軸座標 
  55.      returntrue;   
  56.    }   
  57.    /**  
  58.    * 當用戶按鍵時呼叫  
  59.    */
  60.    @Override
  61.    publicboolean onKeyDown(int keyCode, KeyEvent event) {   
  62.      if(keyCode == KeyEvent.KEYCODE_DPAD_UP){  //當用戶點選↑鍵時 
  63.        y--;  //設定Y軸座標減1 
  64.      }   
  65.      returnsuper.onKeyDown(keyCode, event);   
  66.    }   
  67.    @Override
  68.    publicvoid run() {   
  69.      while (flag) {   
  70.        doDraw(); // 呼叫自定義畫畫方法 
  71.        try {   
  72.          Thread.sleep(2000); // 讓執行緒休息50毫秒 
  73.        } catch (InterruptedException e) {   
  74.          e.printStackTrace();   
  75.        }   
  76.      }   
  77.    }   
  78. }  
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。

[java] view plain copy print?
  1. mHolder = getHolder(); // 獲得SurfaceHolder物件 
  2. mHolder.addCallback(this); // 為SurfaceView新增狀態監聽 
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。

[java] view plain copy print?
  1. IWindowSession mSession;  
IWindowSession mSession;

SurfaceView->updateWindow():

[java] view plain copy print?
  1. relayoutResult = mSession.relayout(  
  2.                        mWindow, mWindow.mSeq, mLayout, mWidth, mHeight,  
  3.                            visible ? VISIBLE : GONE,  
  4.                            WindowManagerGlobal.RELAYOUT_DEFER_SURFACE_DESTROY,  
  5.                            mWinFrame, mOverscanInsets, mContentInsets,  
  6.                            mVisibleInsets, mStableInsets, mConfiguration, mNewSurface);  
 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

[java] view plain copy print?
  1. int relayout(IWindow window, int seq, in WindowManager.LayoutParams attrs,  
  2.            int requestedWidth, int requestedHeight, int viewVisibility,  
  3.            int flags, out Rect outFrame, out Rect outOverscanInsets,  
  4.            out Rect outContentInsets, out Rect outVisibleInsets,  
  5.            out Configuration outConfig, out Surface outSurface);  
 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。

[java] view plain copy print?
  1. mSurface.transferFrom(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指定區域的繪製。