SurfaceView使用与双缓冲技术

为什么引入SurfaceView

  • Android屏幕刷新时间是16ms, 如果View在16ms内完成所需要执行的绘图操作,那么在视觉上,界面是流畅的,否则就会出现卡顿,View,ViewGroup,Animator的代码执行全部是在主线程中完成的,当执行大量逻辑代码的时候,轻则卡顿,甚至会发生ANR问题,所以一般会使用Handler和AsyncTask,但是同时也会加大代码的复杂度,为了解决逻辑及处理带来的时间的耗费,引入了SurfaceView
  • SurfaceView的改进点:
    • 使用双缓冲技术
    • 自带画布,支持在子线程中更新画布内容
  • SurfaceView的缺点:
    • 事件同步问题难处理
使用场景:
  • View:

    • 当界面需要被动更新的时候比如手势交互,因为画面的更新是依赖onTouch来完成的,所以可以直接使用invalidate()函数,在这种情况下两次的Touch之间的事件间隔比较长,不会产生影响
  • SurfaceView:

    • 当界面需要主动更新,比如一个人在跑,需要一个单独的线程不停地重绘人的状态,避免主线程的阻塞
    • 当界面绘制需要频繁刷新,或者刷新时数据处理量比较大时,使用SurfaceView来实现,比如视频播放和摄像头

双缓冲技术

  • 双缓冲技术在原有画布的基础上多增加了一块画布,当需要执行绘图操作的时候,先在缓冲画布上绘制,绘制好后直接将缓冲画布上的内容更新到主画布上,当屏幕更新 时,只需要将缓冲画布上的内容搬过来即可,从而解决了超时处理的问题
  • 双缓冲技术需要两个图形缓冲区
    • 前端缓冲区:当前屏幕正在显示的内容
    • 后端缓冲区:指的是接下来要渲染的图形缓冲区

SurfaceView基本用法:

  • SurfaceView派生自View,所以View中的方法或者实现的自定义控件,SurfaceView都可以实现(此时因为View只能在主线程进行更新,所以此时的SurfaceView也只能在子线程使用
public class SurfaceView extends View{
    
 }
  • 基本实现:
public class SurfaceGesturePath extends SurfaceView {
    private Paint paint;
    private Path path;
    public SurfaceGesturePath(Context context) {
        super(context);
        init();
    }


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


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


    private void init() {
        //如果不加这句话会一直黑屏,这个语句是告知系统哪个控件需要在屏幕重绘时重新绘制,哪些不用
        setWillNotDraw(true);
        paint = new Paint();
        path = new Path();
        paint.setColor(Color.RED);
        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.STROKE);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            path.moveTo(x, y);
            return true;
        }else if(event.getAction() == MotionEvent.ACTION_MOVE){
            path.lineTo(x,y);
        }
        postInvalidate();
        return super.onTouchEvent(event);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);


    }
}

setWillNotDraw(boolean willNotDraw):

  • 这个函数主要用于View派生子类的初始化中,如果参数willNotDraw取true,则表示当前控件没有回执内容,当屏幕重绘时,这个标记为true的控件无需绘制,所以在重绘时也不会掉用那个onDraw()函数,如果设置为false,则在每次刷新屏幕都会重新绘制这个控件(一般来说布局控件他们的构造函数会显示 的设置setWillNotDraw(false);)所以并不建议使用SurfaceView来实现View的自定义控件

使用缓冲绘图

Surface Holdersurface Holder= getHolder();
//获得自带的缓冲画布,并将画布加锁,防止被其他线程更改
Canvas canvas = surfaceHolder.lockCanvas();
....绘图操作
//绘图操作完成,通过SurfaceHolder.unlockCanvasAndPost(canvas)函数将缓冲区画布释放,并将所画内容更新到主线程的画布上,显示在屏幕上
surfaceHolder.unlockCanvasAndPost(canvas);
注意:加锁使用的时候,当画布被其他线程锁定的时候或者缓存Canvas没有创建的时候,surfaceHolder.lockCanvas()函数会返回null, 所以多线程的情况下,不仅要对画布进行判空操作,还需要为画布为空时进行重试策略。
  • 将上述代码改为surfaceView在子线程中更新UI
/**
* surfaceView的正确使用方式
*/
private void drawCanvas() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            SurfaceHolder surfaceHolder = getHolder();
            Canvas canvas = surfaceHolder.lockCanvas();
            canvas.drawPath(path, paint);
            surfaceHolder.unlockCanvasAndPost(canvas);
        }
    }).start();
}

监听Surface生命周期

  • 与SurfaceView相关的三个概念(MVC):

    • Surface Model
    • SurfaceView View
    • SurfaceHolder Controller
  • Surface保存着缓冲画布和绘图内容相关的各种信息,SurfaceView与用户交互负责将Surface中的数据展现在View上,Surface是不允许进行操作的,必须通过Sufaceholder来操作Surface中的数据

  • Android为我们提供了监听Surface生命周期的函数(为了避免获取到空的Canvas)

surfaceHolder.addCallback(new SurfaceHolder.Callback() {
    //当Surface对象被创建的时候,该函数就会被立即调用
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        
    }

    //当Surface发生任何结构性变化的时候(格式或者大小)该函数就会被调用
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {


    }

    //当Surface对象将要销毁时,该函数就会被立即调用
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {


    }
});
  • 一般来说我们在类初始化的时候就立即绘图,那么一般是放在surfaceCreated()函数中开启线程来操作,以防Surface还没有被创建,返回的缓冲画布是空的,在调用SurfaceDestoryed()函数时看线程是否执行完,如果没有执行完就强制取消

SurfaceView的双缓冲技术

  • SurfaceView是使用双缓冲技术来渲染程序UI的, 双缓冲技术需要两个图形缓冲区,其中一个是前端缓冲区,另一个是后端缓冲区,前端缓冲区对应当前屏幕正在显示的内容,而后端缓冲区是接下来要渲染的图形缓冲区。
SurfaceHolder surfaceHolder = getHolder();

//通过这个函数获取的是后端缓冲区
Canvas canvas  = surfaceHolder.lockCanvas();

//通过这个函数是将后端缓冲区与前端缓冲区交换,使得后端缓冲区变为前端缓冲区,将更新的内容显示在屏幕上,而原来的前端缓冲区就变为了后端缓冲区,等待下一次的调用,将其返回给用户,如此往复
surfaceHolder.unlockCanvasAndPost(canvas);
  • 但是正是应为两块画布的交替绘图,会导致两块画布的内容会产生不一致的情况(多线程情况下尤为明显),比如使用一个线程操作A,B两块画布,A是屏幕画布,所以获取的是B画布,更新之后B在前端,A在后端,此时如果线程再次申请画布会获得A画布,如果A画布和B画布上的内容不一致,那么在A画布上继续作画肯定会丢失一些内容

  • 线程获取到的是不同的画布,在不同的画布上绘制,最后呈现的那个画布上的内容一定是不完整的

private void drawText(final SurfaceHolder holder) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            for(int i = 0;i<10;i++){
                Canvas canvas = holder.lockCanvas();
                if(canvas != null){
                    canvas.drawText(i+" ",i * 30, 50, paint);
                }
                holder.unlockCanvasAndPost(canvas);
                try{
                    Thread.sleep(800);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }).start();
}
  • 官方给出的解释:Surface中缓冲画布的数量是根据需求动态分配的,如果用户获取画布的频率较慢,那么将会分配两块缓冲画布;否则,将分配3的倍数块缓冲画布,具体分配多少块视情况而定

猜你喜欢

转载自blog.csdn.net/qq_39424143/article/details/94485251