Android custom View series

  • Android custom View Paint draws text and lines
  • Android custom View Precautions
  • Canvas for Android custom View
  • Android custom View image color processing
  • Android custom View image shape effects – easy to achieve rounded corners and round images
  • Android custom View invalidate method and postInvalidate method
  • Android custom View Window, View View PL and View of the three processes
  • Android custom View event distribution mechanism summary
  • Android custom View requestLayout method and invalidate method

Double buffering mechanism

Origin of the problem

The CPU can access memory much faster than it can access the screen. If a large number of complex images need to be drawn, each time the graphics are read from memory and drawn to the screen, it will cause multiple visits to the screen, resulting in low efficiency. This is like having a level 3 cache between CPU and memory, which needs to improve efficiency.

First layer buffer

Instead of the scheme of drawing images one by one, all images are drawn to a Bitmap object in memory first, and then the Bitmap in memory is drawn to the screen at a time, so as to improve the efficiency of drawing. The onDraw() method of View in Android already implements this layer of buffering. The onDraw() method does not draw and display bits and pieces, but draws and displays them on the screen at once.

Second buffer

The Canvas object of the onDraw() method is associated with the screen, whereas the onDraw() method runs in the UI thread, and if the image to be drawn is too complex, it can cause the application to stall, or even ANR. Therefore, we can first create a temporary Canvas object and draw all images to this temporary Canvas object. After the drawing is completed, the content in this temporary Canvas object (i.e., a Bitmap) can be added. Draw to the canvas in the onDraw() method using the drawBitmap() method. This is equivalent to a Bitmap copy process, which is much more efficient than drawing directly and can reduce the blocking of the UI thread.


SurfaceView

In the SurfaceView, we usually start a child thread, and then in the run method of the child thread, the SurfaceHolder lockCanvas method obtains the Canvas for drawing operation. After drawing, the Canvas is released and the changes are committed using the SurfaceHolder’s unlockCanvasAndPost method.

The characteristics of the SurfaceView

  • The View is mostly for active updates, while the SurfaceView is mostly for passive updates, such as frequent refreshes
  • The View refreshes the screen in the main thread, whereas the SurfaceView usually refreshes the page in a child thread
  • The View does not use double buffering when drawing, whereas the SurfaceView already implements double buffering in the underlying implementation mechanism

Template code for SurfaceView

Public Class MySurfaceView extends SurfaceView implements SurfaceView 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 voidinit() {
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(this);
        setFocusable(true);
//        setFocusableInTouchMode(true);
//        setKeepScreenOn(true); } @override public void surfaceCreated(SurfaceHolder holder) {isDrawing =true; new Thread(this).start(); } // When the SurfaceView view changes, such as vertical/horizontal switching, Override public void surfaceChanged(SurfaceHolder holder, int format, int width, Int height) {} // When the SurfaceView is destroyed, for example, it is not visible, 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); }}}}Copy the code

The above is a common code template for SurfaceView, because SurfaceView is mainly used for video playback and gaming applications, here is a brief introduction, not an in-depth discussion.

(1) SurfaceView must implement the SurfaceHolder Callback interface, which includes surfaceCreated, surfaceChanged and surfaceDestroyed. As the name suggests, this listens to the state of the SurfaceView, similar to the Activity lifecycle.

  • SurfaceCreated methods are invoked when the SurfaceView is created. SurfaceCreated methods perform initialization actions such as setting the flag bits of the drawing thread and creating child threads for drawing
  • The surfaceChanged method is called when the SurfaceView changes state, such as size, format, etc., and the screen is rotated.
  • The surfaceDestroyed method is called when the SurfaceView is destroyed. After the surfaceDestroyed method is called, you cannot do anything with the Surface object, so you need to stop the drawing child thread in the surfaceDestroyed method.

(2) Because SurfaceView is often used in games, videos and other scenes, the drawing operation will be relatively complex, usually need to open the sub-thread, in the sub-thread to perform the drawing operation, so as not to block the UI thread. In the child thread, we obtain the Canvas object through SurfaceHolder’s lockCanvas method for specific drawing operations. At this time, the Canvas object is locked by the current thread. The SurfaceHolder’s unlockCanvasAndPost method is used to submit the drawing result and release the Canvas.

(3) Token parameters used to control child thread drawing, such as the isDrawing variable in the code above, need to be volatile to ensure multithreading safety.

(4) As can be seen from the above code, this is also the embodiment of double buffering by moving the drawing operation to the child thread.

A brief introduction to SurfaceView, SurfaceHolder and Surface

To analyze SurfaceView, we have to analyze it together with other two classes, namely SurfaceHolder and Surface. In fact, there is a typical MVC pattern between these three classes, in which SurfaceView corresponds to View layer. The SurfaceHolder is the Controler interface, and the Surface is the corresponding Model layer, which holds the Canvas and holds the drawn data.

(1) SurfaceView holds SurfaceHolder and Surface. Interfaces in SurfaceHolder can be divided into two categories. One is Callback interface, which is the three interface methods implemented in the template code above. This type of interface is mainly used to listen to the state of the SurfaceView, so that we can do the corresponding processing, such as creating a drawing child thread, stop drawing, etc. Another class of methods are mainly used to interact with Surface and SurfaceView, such as the lockCanvas method and unlockCanvasAndPost method for obtaining Canvas and submitting drawing results.

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(); . }Copy the code

(2) SurfaceView inherits from View, but is actually very different from View. In addition to the SurfaceView features introduced earlier in this article, the underlying SurfaceView is also very different, including its own independent drawing surface. The SurfaceHolder’s lockCanvas method actually calls the Surface’s lockCanvas method and returns the Surface Canvas. And the call procedure adds a reentrant lock, mSurfaceLock. Therefore, the Canvas can only be released after the content of one frame is drawn and changes are submitted during the drawing process, that is, the drawing operation of the next frame can be continued

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, butin 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 CanvaslockCanvas() {
            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() {
            returnmSurfaceFrame; }}; . }Copy the code

(3) Surface implements the Parcelable interface because it needs to transfer between processes and local methods. A Canvas object is created in the Surface to perform specific drawing operations

/** * 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(RectinOutDirty)
            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; }}... }Copy the code

conclusion

  • We learned about double buffering in View drawing and the features and usage of SurfaceView.
  • SurfaceView is mainly used for complex visual effects such as games and videos. It uses double buffering to perform complex drawing operations in child threads to prevent blocking the UI thread.
  • When using SurfaceView, we usually implement the Runnable interface and the SurfaceHolder Callback interface, and open the child thread to perform specific drawing operations
  • Due to the special use of SurfaceView, this paper did not do in-depth analysis, if there is a chance to do video scenes later, I will do in-depth analysis and study.
  • The key is to understand the double buffer mechanism, which is often asked in interviews.

Welcome to follow my wechat public number, and make progress with me every day!