关于Surface的底层双缓冲机制学习

双缓冲机制

问题的由来

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的特殊使用场景,所以本文没有做深入的分析,后面如果有机会做视频方面的场景再来好好深入分析学习。
  • 重点在于双缓冲机制的理解,这个面试时也会经常问道。


作者:xxq2dream
链接:https://www.jianshu.com/p/afe23814b207

上一篇:SDL2常用函数&结构分析:SDL_Surface&SDL_GetWindowSurface&SDL_LoadBMP


下一篇:WPF软件开发系统之二——水环境检测Surface触摸屏软件开发