最近工作中用到了SurfaceView,发现对自己SurfaceView并没有一个系统的认识,而且网上查阅资料也都是一些简单的讲解,因此这里总结一下希望对大家有所帮助。
SurfaceView的基本定义网上有很详细的说明,这里不再进行废话啦。而我对它一个简单理解就是:可以在子线程绘制view的组件,而传统View的绘制都是在UI线程。
网上看到这样一种解释觉得说的也不错:
SurfaceView 就是在Window上挖一个洞,它就是显示在这个洞里,其他的View是显示在Window上,所以View可以显示在 SurfaceView之上,你也可以添加一些层在SurfaceView之上。传统View及其派生类的更新只能在UI线程,然而UI线程还同时处理其他交互逻辑。
此时有的小伙伴会问了,那我们什么时候用SurfaceView,什么时候用传统自定义View呢?
一般我们绘制简单view而且耗时比较短也不需要频繁刷新,传统自定义view就够啦
相反当我们绘制的view比较复杂并且需要频繁刷新,那就用SurfaceView吧。比如:滚动字幕效果实现,小游戏等
定义一个类继承 SurfaceView 实现SurfaceHolder.Callback接口后,有三个回调方法,顺序依次是:
正常初始化3个方法执行顺序是: surfaceCreated -> surfaceChanged -> surfaceDestroyed
界面切换到后台执行: surfaceDestroyed ,返回到当前界面后执行: surfaceCreated -> surfaceChanged
屏幕发生旋转后执行:surfaceDestroyed -> surfaceCreated -> surfaceChanged
SurfaceView 所在的父控件大小发生变化后会执行:surfaceChanged
下面咱们以绘制一个正选曲线为例:
代码如下:
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);
}
}
}
}
这里有几个地方需要注意一下哦:
到这里估计有些小伙伴认为这样写没什么问题吧,复制代码编译运行也没什么问题呢。下面我们这样测试,频繁的切换后台在回到当前绘制界面,相信没几个回合就会报如下错误:
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一样,线程也有生命周期,主要有以下几个阶段:
线程阻塞场景有很多种:
咱们要说的关键方法来了,是它,就是它,join 。根据此方法的特性,我们在surfaceDestroyed方法中对线程执行
mThread.join();
会起到什么作用呢?按道理当mThread调用join方法后,当前线程也就是UI线程会阻塞(至于当前线程为什么会是UI线程,后面会解释哈),等待mThread线程执行完毕后,会停止阻塞。
那如何证明呢?我们简单修改一下代码,首先在mThread.join()方法执行完毕后,打印一行日志
然后我们把子线程的执行时间延长:
最后编译运行项目,初始化界面后再把app切换到后台,日志打印如下:
总结:通过以上方法,我们证明了执行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);
}
}
}
}
篇幅原因,具体代码就不再这里展示了,有兴趣的小伙伴请点击 这里。
如果觉得文章对你有些帮助,麻烦给点个赞吧,十分感谢!
参考文章
详解Java线程中的join()方法
Java中join()方法原理及使用教程