SurfaceView使用小结

前言

Android系统中提供了View可以满足大部分绘图需求,但View主要用于主动更新的情况,用户无法控制其绘制的速度,由于View是通过invalidate方法通知系统去调用view.onDraw方法进行重绘,而Android系统是通过发出VSYNC信号来进行屏幕的重绘,刷新的时间是16ms,如果在16ms内View完成不了执行的操作,用户就会看着卡顿,比如当draw方法里执行的逻辑过多,需要频繁刷新的界面上,例如游戏界面,那么就会不断的阻塞主线程,从而导致画面卡顿。在自定义View的Logcat中,经常看到如下警告:

Skipped 60 frames! The application may be doing too much work onits main thread

Android提供了SurfaceView用于解决这个问题。SurfaceView另开一个绘图线程,它是不会阻碍主线程,并且它在底层实现机制中实现了双缓冲机制。

先看下SDK中对其解释:

SurfaceView是视图(View)的继承类,这个视图里内嵌了一个专门用于绘制的Surface。你可以控制这个Surface的格式和尺寸。Surfaceview控制这个Surface的绘制位置。 
surface是纵深排序(Z-ordered)的,这表明它总在自己所在窗口的后面。surfaceview提供了一个可见区域,只有在这个可见区域内 surface 部分内容才可见,可见区域外的部分不可见。 
surface 的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者 surface 的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。注意,如果 surface 上面有透明控件,那么它的每次变化都会引起框架重新计算它和顶层控件之间的透明效果,这会影响性能。 
你可以通过 surfaceHolder 接口访问这个surface,getHolder() 方法可以得到这个接口。 
surfaceview 变得可见时,surface被创建;surfaceview隐藏前,surface被销毁。这样能节省资源。如果你要查看 surface 被创建和销毁的时机,可以重载surfaceCreated(SurfaceHolder)和 surfaceDestroyed(SurfaceHolder) surfaceView 的核心在于提供了两个线程:UI线程和渲染线程。 
这里应注意: 
1. 所有 SurfaceView 和 SurfaceHolder.Callback 的方法都会在UI线程里调用,一般来说就是应用程序主线程。所以渲染线程所要访问的各种变量应该作同步处理。 
2. 由于surface可能被销毁,它只在SurfaceHolder.Callback.surfaceCreated()和 SurfaceHolder.Callback.surfaceDestroyed()之间有效,所以要确保渲染线程访问的是合法有效的 surface

SurfaceView介绍:

  1. SurfaceView 就是带 Surface 的 view,它是一个 View,是 View 的子类,所以和其他 View 一样,可以在屏幕上展示东西接收用户输入,具有 View 的生命周期回调函数,如 onMeasure、onLayout、onDraw、onTouchEvent 等
  2. SurfaceView 带有独立的 Surface(独立与 window 的 surface),这可以让子线程在独立的 Surface 上面绘制东西,进行 SurfaceView 的界面绘制,这个子线程就叫做渲染线程,但是要让独立的 Surface 上面的东西在 View 上面展示出来,需要 post 一个消息给主线程,目的是把该 Surface 中 canvas 上的东西绘制到 View 的真正的画布上面(window 的 surface 的 canvas上),这样就可以把 UI 线程空闲出来处理用户的交互
  3. Surface 可能被销毁,它只在SurfaceHolder.Callback.surfaceCreated() 和 SurfaceHolder.Callback.surfaceDestroyed() 之间有效,这只是说 Surface 创建和销毁的时候会回到前面两个方法,所以要确保渲染线程访问的是合法有效的 surface
  4. SurfaceHolder.CallBack 是通过 SurfaceView 的 SurfaceHolder 的 addCallback 来设置给 SurfaceHolder 的,让 SurfaceView 实现 CallBack 并设置给 SurfaceHolder,SurfaceView 就可以监听这个独立 Surface 的创建和销毁了。

SurfaceView与View的区别:
 1.View主要适用于主动更新的情况下,而SurfaceView主要适用于被动更新,例如频繁地刷新
2.View在主线程中对画面进行刷新,而SurfaceView通常会通过一个子线程来进行页面的刷新
3.View在绘图时没有使用双缓冲机制,而SufaceView在底层实现机制中就已经实现了双缓冲机制

SurfaceView继承之View,但拥有独立的绘制表面,即它不与其宿主窗口共享同一个绘图表面,可以单独在一个线程进行绘制,并不会占用主线程的资源。这样,绘制就会比较高效,游戏,视频播放,还有最近热门的直播和人脸识别,都可以用SurfaceView。
另外说明一下,SurfaceView有两个子类GLSurfaceView和VideoView

SurfaceView的使用:
   首先SurfaceView也是一个View,它也有自己的生命周期。因为它需要另外一个线程来执行绘制操作,所以我们可以在它生命周期的初始化阶段开辟一个新线程,然后开始执行绘制,当生命周期的结束阶段我们插入结束绘制线程的操作。这些是由其内部一个SurfaceHolder对象完成的。  
 SurfaceView它的绘制原理是绘制前先锁定画布(获取画布),然后等都绘制结束以后在对画布进行解锁 ,最后在把画布内容显示到屏幕上。       

通常情况下,使用以下步骤来创建一个SurfaceView的模板:
创建SurfaceView
创建自定义的SurfaceView继承自SurfaceView,并实现两个接口:SurfaceHolder.Callback和Runnable,通过实现这两个接口,就需要在自定义的SurfaceView中实现接口的方法,对于SurfaceHolder.Callback方法,需要实现如下方法,其实就是SurfaceView的生命周期:

public class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    @Override
    public void surfaceCreated(SurfaceHolder holder) {

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        
    }

    @Override
    public void run() {

    }
初始化SurfaceView:
 在自定义的MySurfaceView的构造方法中,需要对SurfaceView进行初始化,包括SurfaceHolder的初始化、画笔的初始化等。在自定义的SurfaceView中,通常需要定义三个成员变量:
    private SurfaceHolder mHolder;
    private Canvas mCanvas;//绘图的画布
    private volatile boolean mIsDrawing;//控制绘画线程的标志位
  SurfaceHolder,顾名思义,它里面保存了一个对Surface对象的引用,而我们执行绘制方法本质上就是操控Surface。SurfaceHolder因为保存了对Surface的引用,所以使用它来处理Surface的生命周期。(说到底 SurfaceView的生命周期其实就是Surface的生命周期)例如使用 SurfaceHolder来处理生命周期的初始化。
初始化代码如下:
public CustomSurfaceView(Context context) {
        super(context);
        initView();
    }

    public CustomSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public CustomSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        mHolder = getHolder();//获取SurfaceHolder对象
        mHolder.addCallback(this);//注册SurfaceHolder的回调方法
        setFocusable(true);// 能否用键盘获得焦点
        setFocusableInTouchMode(true);//能否通过触摸获得焦点
        this.setKeepScreenOn(true);//屏幕常亮
    }
使用SurfaceView:
 通过SurfaceHolder对象的lockCanvans()方法,我们可以获取当前的Canvas绘图对象。接下来的操作就和自定义View中的绘图操作一样了。需要注意的是这里获取到的Canvas对象还是继续上次的Canvas对象,而不是一个新的对象。因此,之前的绘图操作都会被保留,如果需要擦除,则可以在绘制前,通过drawColor()方法来进行清屏操作(此处有坑,下一篇博客细说)。
 绘制的时候,在surfaceCreated()方法中开启子线程进行绘制,而子线程使用一个while(mIsDrawing)的循环来不停的进行绘制,在绘制的逻辑中通过lockCanvas()方法获取Canvas对象进行绘制,通过unlockCanvasAndPost(mCanvas)方法对画布内容进行提交。整体代码模板如下:

/**
 * Title:
 * Description:
 * Company: 北京****科技有限公司,010-62538800,[email protected]
 *
 * @author Created by ylwang on 2018/2/2
 */


public class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    private SurfaceHolder mHolder;
    private Canvas mCanvas;//绘图的画布
    private volatile boolean mIsDrawing;//控制绘画线程的标志位

    private Paint mPaint;
    private Path mPath;

    public CustomSurfaceView(Context context) {
        super(context);
        initView();
    }

    public CustomSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public CustomSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }


    private void initView() {
        //setLayerType(View.LAYER_TYPE_SOFTWARE, null);//关闭硬件加速
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);//画笔颜色
        mPaint.setAntiAlias(true);//抗锯齿
        mPaint.setStrokeWidth(6);//画笔宽度
        mPaint.setStyle(Paint.Style.STROKE);//空心

        mPath = new Path();

        mHolder = getHolder();//获取SurfaceHolder对象
        mHolder.addCallback(this);//注册SurfaceHolder的回调方法
        setFocusable(true);// 能否用键盘获得焦点
        setFocusableInTouchMode(true);//能否通过触摸获得焦点
        this.setKeepScreenOn(true);//屏幕常亮
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    public static final int TIME_IN_FRAME = 30;

    @Override
    public void run() {
        long start;
        int x = 0, y = 0;
        drawSth();
        while (mIsDrawing) {
            start = SystemClock.uptimeMillis();//开始绘制
            //绘制sin(x)函数
            x += 1;
            y = (int) (100 * Math.sin(x * 2 * Math.PI / 180) + 400);
            mPath.lineTo(x, y);
            drawSth();
            while (SystemClock.uptimeMillis() - start <= TIME_IN_FRAME) {
                Thread.yield();//线程让出
            }
        }
    }


    private void drawSth() {
        try {
            mCanvas = mHolder.lockCanvas(); //拿到当前画布 然后锁定
            mCanvas.drawColor(Color.GRAY);//画布颜色
            mCanvas.drawPath(mPath, mPaint);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (mCanvas != null) {
                mHolder.unlockCanvasAndPost(mCanvas);//保证每次都将绘图的内容提交
            }
        }
    }

}
运行结果如下:
SurfaceView使用小结_第1张图片
解释几点:
1.正弦曲线起点为啥不对?
 Path默认起点是(0,0),所以左上角会连起来。这个自己可以手动设置
2.为什么用Thread.yield():?
 Thread.yield()是暂停当前正在执行的线程对象 ,并去执行其他线程。
 Thread.sleep(long millis):则是使当前线程暂停参数中所指定的毫秒数然后在继续执行线程。
3.TIME_IN_FRAME用来做什么?
 drawSth()方法每一次更新所耗费的时间是不确定的。比如第一次循环drawSth() 耗费了500毫秒 ,第二次循环draw() 耗时2000毫秒。很明显这样就会造成运行刷新时间时快时慢,可能出现卡顿现象。为此最好保证每次刷新的时间是相同的,这样可以保证整体画面过渡流畅。于是理解了TIME_IN_FRAME的意义。

简易画图板实现:
 private int lastX, lastY, dx, dy;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                mPath.moveTo(lastX, lastY);
                break;
            case MotionEvent.ACTION_MOVE:
                dx = x - lastX;
                dy = y - lastY;
                if (dx > 3 || dy > 3) {
                    mPath.quadTo(lastX, lastY, (lastX + x) >> 1, (lastY + y) >> 1);
                }
                lastX = x;
                lastY = y;
                break;
            default:
                break;
        }
        return true;
    }

    @Override
    public void run() {
        long start = 0;
        while (mIsDrawing) {
            start = SystemClock.uptimeMillis();//开始绘制
            drawSth();
            while (SystemClock.uptimeMillis() - start <= TIME_IN_FRAME) {
                Thread.yield();//线程让出
            }
        }
    }
解释一下Path.quadTo(float x1, float y1, float x2, float y2)用于画平滑曲线,该曲线又称为"贝塞尔曲线"(Bezier curve),其中,x1,y1为控制点的坐标值,x2,y2为终点的坐标值;具体参考: Path详细用法 Path类的lineTo和quadTo的区别
简单总结一下:
1. SurfaceView允许其他线程更新视图对象(执行绘制方法),而View只允许UI线程更新视图对象。
2. SurfaceView是放在其他最底层的视图层次中,所有其他视图层都在它上面,所以在它之上可以添加一些层,而且它不能是透明的。(存疑)
3. 它执行动画的效率比View高,而且你可以控制帧数。
4. SurfaceView在绘图时使用了双缓冲机制,而View没有。

下一篇会展示我做人脸识别项目中的爬坑经历。





你可能感兴趣的:(Android)