Android自定義View之雙緩衝機制和SurfaceView

xxq2dream發表於2018-08-08

Android自定義View系列

雙緩衝機制

問題的由來

CPU訪問記憶體的速度要遠遠快於訪問螢幕的速度。如果需要繪製大量複雜的影象時,每次都一個個從記憶體中讀取圖形然後繪製到螢幕就會造成多次地訪問螢幕,從而導致效率很低。這就跟CPU和記憶體之間還需要有三級快取一樣,需要提高效率。

第一層緩衝

在繪製影象時不用上述一個一個繪製的方案,而採用先在記憶體中將所有的影象都繪製到一個Bitmap物件上,然後一次性將記憶體中的Bitmap繪製到螢幕,從而提高繪製的效率。Android中View的onDraw()方法已經實現了這一層緩衝。onDraw()方法中不是繪製一點顯示一點,而是都繪製完後一次性顯示到螢幕。

第二層緩衝

onDraw()方法的Canvas物件是和螢幕關聯的,而onDraw()方法是執行在UI執行緒中的,如果要繪製的影象過於複雜,則有可能導致應用程式卡頓,甚至ANR。因此我們可以先建立一個臨時的Canvas物件,將影象都繪製到這個臨時的Canvas物件中,繪製完成之後再將這個臨時Canvas物件中的內容(也就是一個Bitmap),通過drawBitmap()方法繪製到onDraw()方法中的canvas物件中。這樣的話就相當於是一個Bitmap的拷貝過程,比直接繪製效率要高,可以減少對UI執行緒的阻塞。


SurfaceView

在SurfaceView中,我們一般都會開啟一個子執行緒,然後在子執行緒的run方法中通過SurfaceHolder的lockCanvas方法獲取到Canvas進行繪製操作,繪製完以後再通過SurfaceHolder的unlockCanvasAndPost方法釋放canvas並提交更改。

SurfaceView的特點

  • View主要適用於主動更新的情況下,而SurfaceView主要適用於被動更新,例如頻繁的重新整理
  • View在主執行緒中對畫面進行重新整理,而SurfaceView通常會通過一個子執行緒來進行頁面的重新整理
  • View在繪圖時沒有使用雙緩衝機制,而SurfaceView在底層實現機制中就已經實現了雙緩衝機制

SurfaceView的模版程式碼

//必須實現SurfaceHolder.Callback介面和Runnable介面
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback,Runnable{

    private SurfaceHolder surfaceHolder;
    private Canvas canvas;
    //子執行緒繪製標記
    private volatile boolean isDrawing;

    public MySurfaceView(Context context) {
        super(context);
        init();
    }

    public MySurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(this);
        setFocusable(true);
//        setFocusableInTouchMode(true);
//        setKeepScreenOn(true);

    }


    //當SurfaceView被建立的時候被呼叫
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        isDrawing = true;
        new Thread(this).start();
    }

    //當SurfaceView的檢視發生改變,比如橫豎屏切換時,這個方法被呼叫
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    //當SurfaceView被銷燬的時候,比如不可見了,會被呼叫
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        isDrawing = false;
        surfaceHolder.removeCallback(this);
    }

    @Override
    public void run() {
        while (isDrawing) {
            draw();
        }
    }

    private void draw() {
        try {
            canvas = surfaceHolder.lockCanvas();
            //執行具體的繪製操作

        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (canvas != null) {
                surfaceHolder.unlockCanvasAndPost(canvas);
            }
        }
    }
}
複製程式碼

以上是SurfaceView的一種常見的程式碼模版,因為SurfaceView主要用在視訊播放以及遊戲等應用中,這裡只做一些簡單的介紹,不做深入的探討。

(1)SurfaceView必須實現SurfaceHolder的Callback介面,主要是3個方法,分別是surfaceCreated、surfaceChanged、surfaceDestroyed。從名字就可以看出來這個是監聽SurfaceView狀態的,跟Activity的生命週期有點像。

  • 當SurfaceView被建立時,surfaceCreated方法會被呼叫,surfaceCreated方法中一般做初始化動作,比如設定繪製執行緒的標記位,建立用於繪製的子執行緒等
  • 當SurfaceView的狀態改變時,比如尺寸大小、格式等,常見的操作就是旋轉螢幕了,這個時候surfaceChanged方法會被呼叫。
  • 當SurfaceView被銷燬時,surfaceDestroyed方法會被呼叫。surfaceDestroyed被呼叫後,就不能再對Surface物件進行任何操作,所以我們需要在surfaceDestroyed方法中將繪製的子執行緒停掉。

(2)由於SurfaceView常被用於遊戲、視訊等場景,繪製操作會相對複雜很多,通常都需要開啟子執行緒,在子執行緒中執行繪製操作,以免阻塞UI執行緒。在子執行緒中,我們通過SurfaceHolder的lockCanvas方法獲取Canvas物件來進行具體的繪製操作,此時Canvas物件被當前執行緒鎖定,繪製完成後通過SurfaceHolder的unlockCanvasAndPost方法提交繪製結果並釋放Canvas物件。

(3)用於控制子執行緒繪製的標記引數,如上面程式碼中的isDrawing變數,需要用volatile關鍵字修飾,以保證多執行緒安全。

(4)由上面程式碼可見, 通過將繪製操作移到子執行緒中,這也是雙緩衝的體現。

SurfaceView、SurfaceHolder和Surface的簡單介紹

要分析SurfaceView,就得和其他2個類一起分析,那就是SurfaceHolder和Surface,這3者之間其實是典型的MVC模式,其中SurfaceView對應的就是View層,SurfaceHolder就是controler介面,而Surface就是對應的Model層,它裡面持有Canvas,儲存著繪製的資料。

(1)SurfaceView中持有SurfaceHolder和Surface,SurfaceHolder中的介面可以分為2類,一類是Callback介面,也就是我們上面模版程式碼中實現的3個介面方法,這類介面主要是用於監聽SurfaceView的狀態,以便我們進行相應的處理,比如建立繪製子執行緒,停止繪製等。另一類方法主要用於和Surface以及SurfaceView互動,比如lockCanvas方法和unlockCanvasAndPost方法用於獲取Canvas以及提交繪製結果等。

public interface SurfaceHolder {

    ...

    public interface Callback {

        public void surfaceCreated(SurfaceHolder holder);

        public void surfaceChanged(SurfaceHolder holder, int format, int width,
                int height);

        public void surfaceDestroyed(SurfaceHolder holder);
    }

    public interface Callback2 extends Callback {
        public void surfaceRedrawNeeded(SurfaceHolder holder);
    }

    public void addCallback(Callback callback);

    public void removeCallback(Callback callback);
    
    public Canvas lockCanvas();

    public Canvas lockCanvas(Rect dirty);

    public void unlockCanvasAndPost(Canvas canvas);

    public Surface getSurface();
    
    ...
}
複製程式碼

(2)SurfaceView繼承自View,但是其實和View是有很大的不同的,除了文章前面介紹的幾點SurfaceView的特性外,在底層SurfaceView也很大的不同,包括擁有自己獨立的繪圖表面等。從下面SurfaceView的原始碼中我們可以看到,我們呼叫SurfaceHolder的lockCanvas方法實際上呼叫的是Surface的lockCanvas方法,返回的是Surface中的Canvas。並且呼叫過程加了一個可重入鎖mSurfaceLock。所以繪製過程中只能繪製完一幀內容並提交更改以後才會釋放Canvas,也就是才能繼續下一幀的繪製操作

public class SurfaceView extends View {
    ...

    final Surface mSurface = new Surface(); 
    final ReentrantLock mSurfaceLock = new ReentrantLock();
    
    private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() {

        private static final String LOG_TAG = "SurfaceHolder";

        ...

        @Override
        public void addCallback(Callback callback) {
            synchronized (mCallbacks) {
                // This is a linear search, but in practice we'll
                // have only a couple callbacks, so it doesn't matter.
                if (mCallbacks.contains(callback) == false) {
                    mCallbacks.add(callback);
                }
            }
        }

        @Override
        public void removeCallback(Callback callback) {
            synchronized (mCallbacks) {
                mCallbacks.remove(callback);
            }
        }
        
        @Override
        public Canvas lockCanvas() {
            return internalLockCanvas(null);
        }

        @Override
        public Canvas lockCanvas(Rect inOutDirty) {
            return internalLockCanvas(inOutDirty);
        }

        private final Canvas internalLockCanvas(Rect dirty) {
            mSurfaceLock.lock();

            Canvas c = null;
            if (!mDrawingStopped && mWindow != null) {
                try {
                    c = mSurface.lockCanvas(dirty);
                } catch (Exception e) {
                    Log.e(LOG_TAG, "Exception locking surface", e);
                }
            }

            if (c != null) {
                mLastLockTime = SystemClock.uptimeMillis();
                return c;
            }

            long now = SystemClock.uptimeMillis();
            long nextTime = mLastLockTime + 100;
            if (nextTime > now) {
                try {
                    Thread.sleep(nextTime-now);
                } catch (InterruptedException e) {
                }
                now = SystemClock.uptimeMillis();
            }
            mLastLockTime = now;
            mSurfaceLock.unlock();

            return null;
        }

        @Override
        public void unlockCanvasAndPost(Canvas canvas) {
            mSurface.unlockCanvasAndPost(canvas);
            mSurfaceLock.unlock();
        }

        @Override
        public Surface getSurface() {
            return mSurface;
        }

        @Override
        public Rect getSurfaceFrame() {
            return mSurfaceFrame;
        }
    };
    
    ...
}
複製程式碼

(3)Surface實現了Parcelable介面,因為它需要在程式間以及本地方法間傳輸。Surface中建立了Canvas物件,用於執行具體的繪製操作

/**
 * Handle onto a raw buffer that is being managed by the screen compositor.
 * ...
 */
public class Surface implements Parcelable {

    final Object mLock = new Object(); // protects the native state
    private final Canvas mCanvas = new CompatibleCanvas();
    
    ...
    
    public Canvas lockCanvas(Rect inOutDirty)
            throws Surface.OutOfResourcesException, IllegalArgumentException {
        synchronized (mLock) {
            checkNotReleasedLocked();
            if (mLockedObject != 0) {
                throw new IllegalArgumentException("Surface was already locked");
            }
            mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
            return mCanvas;
        }
    }
    
    public void unlockCanvasAndPost(Canvas canvas) {
        synchronized (mLock) {
            checkNotReleasedLocked();

            if (mHwuiContext != null) {
                mHwuiContext.unlockAndPost(canvas);
            } else {
                unlockSwCanvasAndPost(canvas);
            }
        }
    }

    private void unlockSwCanvasAndPost(Canvas canvas) {
        if (canvas != mCanvas) {
            throw new IllegalArgumentException("canvas object must be the same instance that "
                    + "was previously returned by lockCanvas");
        }
        if (mNativeObject != mLockedObject) {
            Log.w(TAG, "WARNING: Surface's mNativeObject (0x" +
                    Long.toHexString(mNativeObject) + ") != mLockedObject (0x" +
                    Long.toHexString(mLockedObject) +")");
        }
        if (mLockedObject == 0) {
            throw new IllegalStateException("Surface was not locked");
        }
        try {
            nativeUnlockCanvasAndPost(mLockedObject, canvas);
        } finally {
            nativeRelease(mLockedObject);
            mLockedObject = 0;
        }
    }
    
    ...
}
複製程式碼

總結

  • 我們學習了View繪製中的雙緩衝機制,並瞭解了SurfaceView的特性和使用方法。
  • SurfaceView主要用於遊戲、視訊等複雜視覺效果的場景,利用雙緩衝機制,在子執行緒中執行復雜的繪製操作,可以防止阻塞UI執行緒。
  • 我們在使用SurfaceView時一般都要實現Runnable介面和SurfaceHolder的Callback介面,並開啟子執行緒進行具體的繪製操作
  • 因為SurfaceView的特殊使用場景,所以本文沒有做深入的分析,後面如果有機會做視訊方面的場景再來好好深入分析學習。
  • 重點在於雙緩衝機制的理解,這個面試時也會經常問道。

歡迎關注我的微信公眾號,和我一起每天進步一點點!

AntDream

相關文章