天天看點

Android SurfaceView總結及代碼示例

#一.概述

     SurfaceView與普通View不同,View樹上的普通View共享一個Surface,而SurfaceView擁有單獨的Surface。     而且普通View必須在UI線程中繪制,而SurfaceView可以在非UI線程中完成繪制工作,不占用UI主線程。          SurfaceView可以通過SurfaceHolder擷取其Surface的尺寸和狀态變化,并通過SurfaceHolder控制在Surface上的控制流程。     從Android 1.0(API level 1)時就有SurfaceView類。

##.SurfaceView出現的原因

    Android手機上每當顯示屏把對應的幀緩沖區上資料掃描顯示完畢後,系統就會發出一個垂直同步VSYNC信号來觸發下一輪的View重繪。顯示屏的重新整理頻率一般是60Hz,即大約每16ms就會觸發一輪繪制。一般情況下,View在這個時間間隔内是能完成其繪制的。但View是在主線程中繪制,主線程中運作着大量事務,當畫面的繪制邏輯比較複雜、繪制頻率又比較頻繁時,一方面,主線程有可能來不及完成繪制,就會出現畫面卡頓現象;另一方面,過多的繪制任務執行在主線程中,會占用過多的主線程執行時間,妨礙主線程其它任務的執行。     為此,Android中提供了SurfaceView來應對這些場景,它可以在非UI線程中執行繪制任務,不占用主線程的執行時間。至于其繼承類GLSurfaceView,更是引入了OpenGL ES,通過GPU硬體繪制來大大提升繪制速度。 (SurfaceView如果不能及時完成自己的繪制,一樣會畫面卡頓。隻是它可以運作在獨立的線程中完成繪制,不像主線程中有那麼多任務,是以用于繪制的時間更“寬裕”一些。而且就算SurfaceView上畫面卡頓,對主線程也不會造成幹擾,起碼主線程上一切如常,其它View能夠正常重新整理和完成操作響應。)

##.普通 View 和 SurfaceView 的主要差別:

1 . 最本質差别是,普通View必須在主線程中繪制界面,而SurfaceView可以開啟一個新線程來繪制界面。 2 . 是以View适用于耗時較短的繪制,否則容易引發畫面卡頓;      而SurfaceView則适用于較頻繁或耗時較久的繪制,不會是以阻塞UI線程。 3 . SurfaceView由于獨占一個Surface,是以本身具有雙緩沖機制,可以通過傳遞髒區的方式,每次隻進行局部繪制,沒必要全部重新繪制一遍;     而Window上的所有View共享一個Surface,單個普通View并沒有雙緩沖機制,每次繪制必須完全重新繪制一遍,不能隻繪制View的局部。

##.SurfaceView單獨擁有的Surface是如何顯示的

    SurfaceView本身直接管理一個獨立的Surface,同時SurfaceView又屬于某個View樹,附在對應的Window上,是以它與兩個Surface相關聯:         一個是SurfaceView自己單獨擁有的Surface,其顯示層級較低;         另一個是View樹所在Window的Surface,其顯示層級較高。 是以,前者其實顯示在後者的下面。     SurfaceView真實畫面繪制在自己獨自擁有的Surface上,而這張Surface位于Window下面,為何沒被遮擋,而是會顯示出來的呢?     因為Window上的View樹繪制時,SurfaceView也會參與,它會把Window的Surface上自己對應的區域繪制成透明色,于是Window下面SurfaceView獨立的Surface就可以顯示出來了。這個過程,就如同在Window上對應位置嵌了塊透明玻璃一樣,透過透明玻璃可以看到玻璃下面的東西。

二、SurfaceView的雙緩沖

    SurfaceView實際上是利用了Surface的雙緩沖機制,其SurfaceHolder的lockCanvas()和unlockCanvasAndPost()在實作中其實最終是調用Surface的相應方法來完成功能。     其雙緩沖機制,可以簡化了解為有兩個緩沖區引用,一個frontBuffer和一個backBuffer,backBuffer指向後置緩沖區,用于緩存正在繪制的畫面;而frontBuffer指向前置緩沖區,用于緩存最近繪制完畢、要送出使用的畫面。二者可以互相切換。  
繪制中 不斷地循環這個過程: 1.當使用lockCanvas()擷取畫布,擷取Surface的Canvas對象,用于在backBuffer上進行繪制新的内容; 2. 當上述繪圖結束 後,調用 unlockCanvasAndPost(canvas),互換二者身份,原來的backBuffer變為frontBuffer用于送出給外部使用,而原來的frontBuffer變為backBuffer, 等待下一次 lockCanvas() 并參與繪制新内容 。    其中步驟1可以使用lockCanvas(Rect dirty)傳入一個區域範圍,這個範圍一般稱為”髒區“,通過髒區,可以告訴SurfaceView本輪想去重繪哪部分範圍的畫面。(這名字挺形象的,這部分區域髒了,是以需要抹幹淨重新繪制。)  當然,也可以直接調用lockCanvas(),此時在實作中其實傳入的髒區其實為null,這代表着整個backBuffer都是髒區,需要完全重繪一遍。  
   在lockCanvas()或lockCanvas(Rect dirty)對應的native實作邏輯中,會判斷是否能将frontBuffer中的圖像複制到backBuffer中,如果可以的話,會把frontBuffer中”上一輪髒區 - 本輪髒區“ 對應範圍的畫面複制到backBuffer上。這樣在上面不斷循環的繪制中,其實每一輪所需要繪制的,隻是每一輪髒區内的範圍,髒區外的範圍會保持跟上一次畫面相同。    上述是否能将frontBuffer中的圖像複制到backBuffer中的判斷條件是:frontBuffer已存在 且 前後緩沖區寬、高和格式都完全一緻。 
   至于為何每次複制範圍是”上一輪髒區 - 本輪髒區“,可以這麼了解:    每一輪在原有基礎上隻有髒區内容發生了改變。是以當本輪需要在backBuffer上繪制時,frontBuffer上隻有上一輪髒區的内容是針對目前backBuffere内容做的改變,隻要把這部分内容複制過來,backBuffer上畫面就與frontBuffer中的上一輪繪制結果完全相同。但本輪會重繪本輪髒區内容,是以隻需要複制frontBuffer中”上一輪髒區 - 本輪髒區“ 對應範圍的畫面。
   通過指定髒區,每輪隻需要局部繪制,這算是SurfaceView雙緩沖特性的一個重要應用。普通View在自己的繪制流程中無雙緩沖特性,如果需要重繪,就必須完全繪制一遍。 (但Window的整體畫面對應一個Surface,也有雙緩沖特性,ViewRootImpl内部會記錄每一輪需要重繪的髒區,每次View樹繪制隻會重繪需要繪制的View。無論SurfaceView還是普通View所依附的Window,其畫面載體都是Surface,當然都可以利用Surface的雙緩沖特性。)

三、 相關 重要API     SurfaceView持有一個SurfaceHolder,而SurfaceHolder中持有一個Surface,ViewHolder就像是一個Surface的管理器,可以監聽器狀态改變并針對其做一些操作。 ###.SurfaceHolder

   SurfaceHolder是一個接口,類似于一個surace的監聽器。通過下面三個回調方法監聽Surface的建立、銷毀或者改變。       SurfaceView中調用getHolder方法,可以獲得目前SurfaceView中的surface對應SurfaceHolder。
SurfaceHolder中重要的方法有: 1. void addCallback(SurfaceHolder.Callback callback );     為SurfaceHolder添加一個SurfaceHolder.Callback回調接口。 2. Canvas lockCanvas() ; 3. Canvas lockCanvas(Rect dirty)    調用後可擷取Canvas用于繪制。    實際執行邏輯是在native層完成的,在native層,會為Surface的backBuffer配置設定可用圖形緩沖區,把這個圖像緩沖區作為畫布建立Canvas對象,并傳回給java層。 4. abstract  void unlockCanvasAndPost(Canvas canvas);     繪制完成後調動。        實際執行邏輯是在native層完成的,在native層會将目前繪制好的後置緩沖區送出供畫面消費方使用。BufferQueue中隻有兩個GraphicBuffer時,這一步加上下一次的lockCanvas(),最後總的效果是互換了前後緩沖區。

###.SurfaceHolder.Callback

    SurfaceHolder.Callback是SurfaceHolder接口内部的靜态子接口,可用于監聽持有的Surface狀态變化,SurfaceHolder.Callback中定義了三個接口方法: 1: public void surfaceCreated(SurfaceHolder holder);                 //Surface建立後觸發,一般在這裡啟動繪制畫面的線程。                 Surface開始顯示,會觸發Surface的建立,例如Activity從背景不顯示切換回前台顯示。 2:public void sufaceChanged(SurfaceHolder holder,int format,int width,int height);                //Surface的大小、資料格式發生改變時調用。 3: public void surfaceDestroyed(SurfaceHolder holder);                //銷毀時激發,一般在這裡将繪制畫面的線程停止、釋放。                   Surface不顯示,會觸發Surface的銷毀,例如Activity從前台切換到背景不再顯示。

###.SurfaceView類中的API

1.SurfaceHolder getHolder()     擷取SurfaceView中的SurfaceHolder對象; 2.setZOrderOnTop(boolean onTop)     控制SurfaceView的Surface是否置于其所屬Window的上方(預設是在Window下方的)。 (“Control whether the surface view's surface is placed on top of its window.”) 3.setZOrderMediaOverlay(boolean isMediaOverlay)     Window上可能會添加多個SurfaceView或TextureView,這些特殊View内部都含有自己獨立的Surface。     而這個方法的作用是,控制該SurfaceView的Surface是否置于其它這些Surface的上方,但仍然會在Window下方。 (“ Control whether the surface view's surface is placed on top of another regular surface view in the window (but still behind the window itself).”)

四、代碼示例

public class TestSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
    private final String TAG = getClass().getSimpleName();
    private SurfaceHolder mHolder = getHolder();

    public TestSurfaceView(Context context) {
        this(context, null);
    }

    public TestSurfaceView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public TestSurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        //為Surface添加狀态監聽
        mHolder.addCallback(this);
    }

    /*********單獨的線程用于繪制********/
    //這裡使用Timer來進行控制,友善控制間隔時間
    //Timer中包含一個TimerThread線程,它繼承自Thread,任務都是在TimerThread線程中執行的
    private Timer mTimer;
    private TimerTask mTimerTask;
    private long mPeriod = 1000/30;//定義重新整理間隔為1000/30ms,即每秒鐘重新整理30次

    /**********     繼承自SurfaceHolder.Callback的三個方法        **********/
    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        ILog.d(TAG, "surfaceCreated()");

        initDrawSetting();
        //開始在TimerThread線程中執行繪制操作
        mTimer = new Timer();//每次都要建立,因為一旦cancel,就不能再次start()使用
        mTimerTask = new TimerTask() {
            @Override
            public void run() {
                timeNow += mPeriod;
                draw();
            }
        };
        mTimer.schedule(mTimerTask, 0, mPeriod);
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
        ILog.d(TAG, "surfaceChanged()");
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        ILog.d(TAG, "surfaceDestroyed()");

        if(mTimer != null){
            mTimer.cancel();
        }
    }


    /***************      繪制邏輯       ****************/
    private Paint mPaint;//畫筆
    //初始化畫筆
    private void initDrawSetting(){
        if(mPaint == null){
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint.setTextSize(DeviceUtils.spToPx(16));
            Typeface font = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
            mPaint.setTypeface( font );
            mPaint.setStrokeWidth(DeviceUtils.dpToPx(0.5f));
        }
        textWidth = mPaint.measureText(text);
    }
    private long totalTime = 5000;
    private long timeNow = 0;
    private int startX = DeviceUtils.dpToPx(10);
    private String text = "種一棵樹,最好的時間是十年前,其實是現在";
    private float textWidth;
    private int lineHeight = DeviceUtils.dpToPx(100);
    private int textBaseline = lineHeight/2;
    private int mNormalTextColor = getResources().getColor(R.color.common_white, null);
    private int mHighlightTextColor = getResources().getColor(R.color.common_red, null);;
    private int mStrokeTextColor = Color.parseColor("#66000000");
    private int mBgColor = getResources().getColor(R.color.common_white60, null);
    //自定義的方法,封裝繪制邏輯
    private void draw(){
        //1.鎖定畫布,将在背景緩沖區中做修改
        Canvas canvas = mHolder.lockCanvas();

        //2.具體的繪制
        //這裡是模拟卡拉ok時一行歌詞從左到右逐漸變高亮的過程
        float highlightTextWidth = textWidth * (timeNow%totalTime)/totalTime;
//        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//清除背景
        canvas.drawColor(mBgColor, PorterDuff.Mode.SRC);//設定背景顔色
        //首先,确定左邊文字選中的裁剪區域,然後用高亮色繪制文字
        canvas.save();
        canvas.clipRect(startX, 0, startX + highlightTextWidth, lineHeight);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(mStrokeTextColor);
        canvas.drawText(text, startX, textBaseline, mPaint);
        mPaint.setColor(mHighlightTextColor);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawText(text, startX, textBaseline, mPaint);
        canvas.restore();
        //确定右邊文字的選中裁剪區域,緊鄰左邊已繪制的文字,然後用普通色繪制文字。
        //這樣最終效果是,一行文字,左邊部分是高亮色,右邊文字是普通色
        canvas.save();
        canvas.clipRect(startX + highlightTextWidth, 0, startX + textWidth, lineHeight);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(mStrokeTextColor);
        canvas.drawText(text, startX, textBaseline, mPaint);
        mPaint.setColor(mNormalTextColor);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawText(text, startX, textBaseline, mPaint);
        canvas.restore();

        //3.将畫布的内容儲存到背景緩沖區,然後将背景緩沖區切換到前台并顯示在surface中
        //原先的前台緩沖區将切換為背景緩沖區
        mHolder.unlockCanvasAndPost(canvas);
    }
}
           

相關筆記: Android Surface & Canvas簡介_丞恤猿的部落格-CSDN部落格 參考剪藏: SurfaceView的源代碼: http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/view/SurfaceView.java (AndroidStudio中隻能看到SurfaceView部分方法的方法名,看不到完整源代碼)

Android-Surface之雙緩沖及SurfaceView解析 - 部落格 - 程式設計圈