【Android 你的SurfaceView休眠了吗】

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

最近工作中用到了SurfaceView,发现对自己SurfaceView并没有一个系统的认识,而且网上查阅资料也都是一些简单的讲解,因此这里总结一下希望对大家有所帮助。

SurfaceView 介绍

SurfaceView的基本定义网上有很详细的说明,这里不再进行废话啦。而我对它一个简单理解就是:可以在子线程绘制view的组件,而传统View的绘制都是在UI线程。
网上看到这样一种解释觉得说的也不错:

SurfaceView 就是在Window上挖一个洞,它就是显示在这个洞里,其他的View是显示在Window上,所以View可以显示在 SurfaceView之上,你也可以添加一些层在SurfaceView之上。传统View及其派生类的更新只能在UI线程,然而UI线程还同时处理其他交互逻辑。

SurfaceView使用

此时有的小伙伴会问了,那我们什么时候用SurfaceView,什么时候用传统自定义View呢?
一般我们绘制简单view而且耗时比较短也不需要频繁刷新,传统自定义view就够啦
相反当我们绘制的view比较复杂并且需要频繁刷新,那就用SurfaceView吧。比如:滚动字幕效果实现,小游戏等

基本使用

定义一个类继承 SurfaceView 实现SurfaceHolder.Callback接口后,有三个回调方法,顺序依次是:

  • surfaceCreated 每次界面可见,都会回调
  • surfaceChanged 每次视图大小发生改变,都会回调
  • surfaceDestroyed 每次界面不可见,都会回调

正常初始化3个方法执行顺序是: surfaceCreated -> surfaceChanged -> surfaceDestroyed
界面切换到后台执行: surfaceDestroyed ,返回到当前界面后执行: surfaceCreated -> surfaceChanged
屏幕发生旋转后执行:surfaceDestroyed -> surfaceCreated -> surfaceChanged
SurfaceView 所在的父控件大小发生变化后会执行:surfaceChanged

下面咱们以绘制一个正选曲线为例:

效果图:
【Android 你的SurfaceView休眠了吗】_第1张图片

代码如下:

package com.lovol.surfaceviewdemo.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * 绘制正选曲线
 */
public class SurfaceViewSinFun extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    private static final String TAG = "SurfaceViewSinFun";
    private Thread mThread;
    private SurfaceHolder mSurfaceHolder;
    //绘图的Canvas
    private Canvas mCanvas;
    //子线程标志位
    private boolean mIsDrawing;
    private int x = 0, y = 0;
    private Paint mPaint;
    private Path mPath;

    public SurfaceViewSinFun(Context context) {
        this(context, null);
    }

    public SurfaceViewSinFun(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SurfaceViewSinFun(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(5);
        mPath = new Path();
        //路径起始点(0, 100)
        mPath.moveTo(0, 100);
        initView();
    }

    /**
     * 初始化View
     */
    private void initView() {
        mSurfaceHolder = getHolder();
        mSurfaceHolder.addCallback(this);
        setFocusable(true);
        setKeepScreenOn(true);
        setFocusableInTouchMode(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.i(TAG, "surfaceCreated: ");
        mIsDrawing = true;
        mThread= new Thread(this);
        mThread.start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        Log.i(TAG, "surfaceCreated: width=" + width + " height=" + height);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.i(TAG, "surfaceDestroyed: ");
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            drawSomething();
            x += 1;
            y = (int) (100 * Math.sin(2 * x * Math.PI / 180) + 400);
            //加入新的坐标点
            mPath.lineTo(x, y);
        }
    }

    private void drawSomething() {
        drawView();
    }
    private void drawView() {
        try {
            //获取一个 Canvas 对象,
            mCanvas = mSurfaceHolder.lockCanvas();
            synchronized (mSurfaceHolder) {
                if (mCanvas != null) {
                    //绘制背景
                    mCanvas.drawColor(Color.WHITE);
                    //绘制路径
                    mCanvas.drawPath(mPath, mPaint);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (mCanvas != null) {
                //释放canvas对象并提交画布
                mSurfaceHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

这里有几个地方需要注意一下哦:

  1. 在使用 mCanvas 进行绘制时和释放canvas对象并提交画布时要对mCanvas进行判空处理
  2. 用mCanvas绘制时,先绘制一个背景色 mCanvas.drawColor(Color.WHITE);

发现问题

到这里估计有些小伙伴认为这样写没什么问题吧,复制代码编译运行也没什么问题呢。下面我们这样测试,频繁的切换后台在回到当前绘制界面,相信没几个回合就会报如下错误:

java.lang.IllegalStateException: Surface has already been released.
        at android.view.Surface.checkNotReleasedLocked(Surface.java:801)
        at android.view.Surface.unlockCanvasAndPost(Surface.java:478)
        at android.view.SurfaceView$1.unlockCanvasAndPost(SurfaceView.java:1757)
或者
java.lang.IllegalStateException: Surface has already been lockCanvas. 

在执行surfaceDestroyed方法时已经进行 mIsDrawing = false, while循环肯定停止了,drawView方法应该不会在执行了吧。怎么还会报这样的错误呢?

初步解决问题

对surfaceView足够了解的小伙伴应该会说,需要在子线程绘制view的时候,让线程适当的进行休眠,控制一下绘制的频率。没错,确实需要这样处理,我们在drawSomething方法中修改,代码改进如下:

 //帧速率
 private static final long FRAME_RATE = 30;
 private void drawSomething() {
        long startTime = System.currentTimeMillis();
        drawView();

        //需要计算绘制所需的时间,并休眠一段时间以维持一定的帧率
        long endTime = System.currentTimeMillis();
        long timeDiff = endTime - startTime;
        long sleepTime = FRAME_RATE - timeDiff;
        try {
            if (sleepTime > 0) {
               // System.out.println("SurfaceViewSinFun.drawSomething sleepTime=" + sleepTime);
                Thread.sleep(sleepTime);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

修改后我们再编译运行,同样进行频繁的切换后台测试,多次测试后确实没出现开始那样的报错。说明这样的改动,效果不错!

线程知识

不过在我进行长达几十次的测试后,偶尔发现还是会报上文中的错误。这又是为什么呢?我们是在子线程绘制的view,当我们切换后台在回到当前界面,在surfaceCreated方法中线程又在不断的新建,难道是线程哪里没有处理好导致的?带着这个疑问,我们一起回顾一下线程知识吧。
深入的了解线程不是本文的重点,咱们重点说一下关键的方法。就像Android中的activity一样,线程也有生命周期,主要有以下几个阶段:

  1. 新建(new Thread)
  2. 开启(start):调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段
  3. 运行(run):当就绪的线程被调度并获得CPU资源时,便进入运行状态
  4. 阻塞(blocked):线程阻塞场景有很多种
  5. 销毁(Terminated):线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁

线程阻塞(blocked)

线程阻塞场景有很多种:

  1. 等待I/O流的输入输出
  2. 网络请求
  3. 调用sleep()方法,sleep时间结束后阻塞停止
  4. 调用wait()方法,调用notify()唤醒线程后,阻塞停止
  5. 其他线程执行join()方法,当前线程则会阻塞,需要等其他线程执行完毕。

咱们要说的关键方法来了,是它,就是它,join 。根据此方法的特性,我们在surfaceDestroyed方法中对线程执行

mThread.join(); 

会起到什么作用呢?按道理当mThread调用join方法后,当前线程也就是UI线程会阻塞(至于当前线程为什么会是UI线程,后面会解释哈),等待mThread线程执行完毕后,会停止阻塞。
那如何证明呢?我们简单修改一下代码,首先在mThread.join()方法执行完毕后,打印一行日志
【Android 你的SurfaceView休眠了吗】_第2张图片
然后我们把子线程的执行时间延长:
【Android 你的SurfaceView休眠了吗】_第3张图片

最后编译运行项目,初始化界面后再把app切换到后台,日志打印如下:

在这里插入图片描述
我们只需关注打印日志的2个地方:

  1. 从日志打印我们知道,当前UI线程是3055,surfaceCreated,surfaceChanged,surfaceDestroyed三个方法线程id也都是3055,也都是在UI线程,这也是为什么会说当前线程是UI线程了。而我们绘制view的子线程id是3086。
  2. surfaceDestroyed方法中执行mThread.join()方法后,UI线程确实阻塞了将近20s,之后才继续后面的日志打印。

总结:通过以上方法,我们证明了执行mThread.join()方法后,当前线程也就是UI线程会阻塞,等待mThread线程执行完毕后,才会停止阻塞。

最终解决问题

有了对线程的join方法了解之后,我们对surfaceDestroyed方法中的代码,改进如下:

@Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.i(TAG, "surfaceDestroyed: ");
        // 结束线程
        boolean retry = true;
        while (retry) {
            try {
                mIsDrawing = false;
                mThread.join();
                retry = false;
            } catch (InterruptedException e) {
                //e.printStackTrace();
                // 如果线程无法正常结束,则继续重试
            }
        }

    }

经过这样的改进之后,不管我们如何频繁的前后台切换,相信都不会在报错啦!我们的代码质量也会更好,不过代码质量目前是最好的吗?如果你看过郭老师的这篇文章 volatile关键字在Android中到底有什么用? 会发现目前代码还不够完美,其实我们最好把 mIsDrawing 变量用 volatile 修饰一下,像这样:

    //子线程标志位
    private volatile boolean mIsDrawing;

至于为什么加volatile关键字,郭老师文章中讲的很详细啦,感兴趣的小伙伴去看看吧。到这里我们的问题就算解决啦,完整的代码如下:

package com.lovol.surfaceviewdemo.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * 标准 SurfaceView 的用法
 * 绘制正选曲线
 */
public class SurfaceViewSinFun extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    private static final String TAG = "SurfaceViewSinFun";

    //帧速率
    private static final long FRAME_RATE = 30;

    private Thread mThread;
    private SurfaceHolder mSurfaceHolder;
    //绘图的Canvas
    private Canvas mCanvas;
    //子线程标志位
    private volatile boolean mIsDrawing;
    private int x = 0, y = 0;
    private Paint mPaint;
    private Path mPath;

    public SurfaceViewSinFun(Context context) {
        this(context, null);
    }

    public SurfaceViewSinFun(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SurfaceViewSinFun(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(5);
        mPath = new Path();
        //路径起始点(0, 100)
        mPath.moveTo(0, 100);
        initView();
    }

    /**
     * 初始化View
     */
    private void initView() {
        mSurfaceHolder = getHolder();
        mSurfaceHolder.addCallback(this);
        setFocusable(true);
        setKeepScreenOn(true);
        setFocusableInTouchMode(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.i(TAG, "surfaceCreated: ");
        mIsDrawing = true;
        mThread= new Thread(this);
        mThread.start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        Log.i(TAG, "surfaceChanged: width=" + width + " height=" + height);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.i(TAG, "surfaceDestroyed: ");
        // 结束线程
        boolean retry = true;
        while (retry) {
            try {
                mIsDrawing = false;
                mThread.join();
                retry = false;
            } catch (InterruptedException e) {
                //e.printStackTrace();
                // 如果线程无法正常结束,则继续重试
            }
        }

    }

    @Override
    public void run() {
        while (mIsDrawing) {
            drawSomething();
            x += 1;
            y = (int) (100 * Math.sin(2 * x * Math.PI / 180) + 400);
            //加入新的坐标点
            mPath.lineTo(x, y);
        }
    }

    /**
     * 核心方法 1:
     *
     * 使用 SurfaceHolder 的 lockCanvas() 方法获取一个 Canvas 对象,
     * 并在同步块中来绘制游戏界面,最后使用 SurfaceHolder 的 unlockCanvasAndPost() 方法释放 Canvas 对象并提交绘制结果。
     * 在绘制完成后,我们需要计算绘制所需的时间,并休眠一段时间以维持一定的帧率。
     */
    private void drawSomething() {
        long startTime = System.currentTimeMillis();
        drawView();

        //需要计算绘制所需的时间,并休眠一段时间以维持一定的帧率
        long endTime = System.currentTimeMillis();
        long timeDiff = endTime - startTime;
        long sleepTime = FRAME_RATE - timeDiff;
        try {
            if (sleepTime > 0) {
               // System.out.println("SurfaceViewSinFun.drawSomething sleepTime=" + sleepTime);
                Thread.sleep(sleepTime);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 核心方法 2
     */
    private void drawView() {
        try {
            //获取一个 Canvas 对象,
            mCanvas = mSurfaceHolder.lockCanvas();
            synchronized (mSurfaceHolder) {
                if (mCanvas != null) {
                    //绘制背景
                    mCanvas.drawColor(Color.WHITE);
                    //绘制路径
                    mCanvas.drawPath(mPath, mPaint);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (mCanvas != null) {
                //释放canvas对象并提交画布
                mSurfaceHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }


}

基于surfaceView文字滚动效果

先看效果:
【Android 你的SurfaceView休眠了吗】_第4张图片

篇幅原因,具体代码就不再这里展示了,有兴趣的小伙伴请点击 这里。

如果觉得文章对你有些帮助,麻烦给点个赞吧,十分感谢!

源码


参考文章
详解Java线程中的join()方法
Java中join()方法原理及使用教程

你可能感兴趣的:(【android】,android,ui,android,studio)