> 文章列表 > 【Android 你的SurfaceView休眠了吗】

【Android 你的SurfaceView休眠了吗】

【Android 你的SurfaceView休眠了吗】

最近工作中用到了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休眠了吗】

代码如下:

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;//绘图的Canvasprivate 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);}@Overridepublic void surfaceCreated(SurfaceHolder holder) {Log.i(TAG, "surfaceCreated: ");mIsDrawing = true;mThread= new Thread(this);mThread.start();}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {Log.i(TAG, "surfaceCreated: width=" + width + " height=" + height);}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {Log.i(TAG, "surfaceDestroyed: ");mIsDrawing = false;}@Overridepublic 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线程会阻塞,等待mThread线程执行完毕后,会停止阻塞。

最终解决问题

surfaceDestroyed方法中的代码,改进如下:

@Overridepublic 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();// 如果线程无法正常结束,则继续重试}}}

到这里我们的问题就算解决啦。完整的代码如下:

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;//绘图的Canvasprivate 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);}@Overridepublic void surfaceCreated(SurfaceHolder holder) {Log.i(TAG, "surfaceCreated: ");mIsDrawing = true;mThread= new Thread(this);mThread.start();}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {Log.i(TAG, "surfaceChanged: width=" + width + " height=" + height);}@Overridepublic 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();// 如果线程无法正常结束,则继续重试}}}@Overridepublic 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休眠了吗】

具体代码如下:

package com.lovol.surfaceviewdemo.view;import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff.Mode;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import com.lovol.surfaceviewdemo.R;/* 文字滚动效果*/
public class ScrollTextSimpleView extends SurfaceView implements SurfaceHolder.Callback, Runnable {private final String TAG = "ScrollTextSimpleView";private SurfaceHolder surfaceHolder;private Paint paint = null;private boolean stopScroll = false;     // stop scrollprivate boolean pauseScroll = false;    // pause scrollprivate boolean clickEnable = false;    // click to stop/startprivate int speed = 4;                  // scroll-speedprivate String text = "";               // scroll textprivate float textSize = 20f;           // default text sizeprivate int textColor;private int textBackColor = 0x00000000;private int needScrollTimes = Integer.MAX_VALUE;      //scroll timesprivate int viewWidth = 0;private int viewHeight = 0;private float textWidth = 0f;private float textX = 0f;private float textY = 0f;private float viewWidth_plus_textLength = 0.0f;private Thread mThread;boolean isSetNewText = false;boolean isScrollForever = true;public ScrollTextSimpleView(Context context) {super(context);}/* constructs 2 @param context CONTEXT* @param attrs   ATTRS*/public ScrollTextSimpleView(Context context, AttributeSet attrs) {super(context, attrs);surfaceHolder = this.getHolder();  //get The surface holdersurfaceHolder.addCallback(this);paint = new Paint();TypedArray arr = getContext().obtainStyledAttributes(attrs, R.styleable.ScrollTextView);clickEnable = arr.getBoolean(R.styleable.ScrollTextView_clickEnable, clickEnable);speed = arr.getInteger(R.styleable.ScrollTextView_speed, speed);text = arr.getString(R.styleable.ScrollTextView_text);textColor = arr.getColor(R.styleable.ScrollTextView_text_color, Color.BLACK);textSize = arr.getDimension(R.styleable.ScrollTextView_text_size, textSize);needScrollTimes = arr.getInteger(R.styleable.ScrollTextView_times, Integer.MAX_VALUE);isScrollForever = arr.getBoolean(R.styleable.ScrollTextView_isScrollForever, true);paint.setColor(textColor);paint.setTextSize(textSize);paint.setFlags(Paint.ANTI_ALIAS_FLAG);paint.setAntiAlias(true);paint.setFilterBitmap(true);setZOrderOnTop(true);  //Control whether the surface view's surface is placed on top of its window.getHolder().setFormat(PixelFormat.TRANSLUCENT);setFocusable(true);arr.recycle();}/* measure text height width @param widthMeasureSpec  widthMeasureSpec* @param heightMeasureSpec heightMeasureSpec*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int mHeight = getFontHeight(textSize);      //实际的视图高viewWidth = MeasureSpec.getSize(widthMeasureSpec);viewHeight = MeasureSpec.getSize(heightMeasureSpec);// when layout width or height is wrap_content ,should init ScrollTextView Width/Heightif (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {setMeasuredDimension(viewWidth, mHeight);viewHeight = mHeight;} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {setMeasuredDimension(viewWidth, viewHeight);} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {setMeasuredDimension(viewWidth, mHeight);viewHeight = mHeight;}}@Overridepublic void surfaceCreated(SurfaceHolder holder) {stopScroll = false;mThread = new Thread(this);mThread.start();Log.i(TAG, "surfaceCreated is created");}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {Log.i(TAG, "surfaceChanged: width=" + width + " height=" + height);}@Overridepublic void surfaceDestroyed(SurfaceHolder arg0) {// 结束线程boolean retry = true;while (retry) {try {stopScroll = true;mThread.join();retry = false;} catch (InterruptedException e) {// 如果线程无法正常结束,则继续重试}}Log.i(TAG, "surfaceDestroyed is destroyed");}/* text height @param fontSize fontSize* @return fontSize`s height*/private int getFontHeight(float fontSize) {Paint paint = new Paint();paint.setTextSize(fontSize);FontMetrics fm = paint.getFontMetrics();return (int) Math.ceil(fm.descent - fm.ascent);}/* get Background color @return textBackColor*/public int getBackgroundColor() {return textBackColor;}/* set background color @param color textBackColor*/public void setScrollTextBackgroundColor(int color) {this.setBackgroundColor(color);this.textBackColor = color;}/* get speed @return speed*/public int getSpeed() {return speed;}/* get Text @return*/public String getText() {return text;}/* get text size @return px*/public float getTextSize() {return px2sp(this.getContext(), textSize);}/* get text color @return textColor*/public int getTextColor() {return textColor;}/* set scroll times @param times scroll times*/public void setTimes(int times) {if (times <= 0) {throw new IllegalArgumentException("times was invalid integer, it must between > 0");} else {needScrollTimes = times;isScrollForever = false;}}/* set scroll text size SP @param textSizeTem scroll times*/public void setTextSize(float textSizeTem) {if (textSize < 20) {throw new IllegalArgumentException("textSize must  > 20");} else if (textSize > 900) {throw new IllegalArgumentException("textSize must  < 900");} else {this.textSize = sp2px(getContext(), textSizeTem);//重新设置Sizepaint.setTextSize(textSize);//视图区域也要改变measureVarious();//实际的视图高,thanks to WGint mHeight = getFontHeight(textSizeTem);ViewGroup.LayoutParams lp = this.getLayoutParams();lp.width = viewWidth;lp.height = dip2px(this.getContext(), mHeight);this.setLayoutParams(lp);isSetNewText = true;}}/* dp to px @param context c* @param dpValue dp* @return*/private int dip2px(Context context, float dpValue) {final float scale = context.getResources().getDisplayMetrics().density;return (int) (dpValue * scale + 0.5f);}/* sp to px @param context c* @param spValue sp* @return*/private int sp2px(Context context, float spValue) {float fontScale = context.getResources().getDisplayMetrics().scaledDensity;return (int) (spValue * fontScale + 0.5f);}public int px2sp(Context context, float pxValue) {float fontScale = context.getResources().getDisplayMetrics().scaledDensity;return (int) (pxValue / fontScale + 0.5f);}/* set scroll text @param newText scroll text*/public void setText(String newText) {isSetNewText = true;stopScroll = false;this.text = newText;measureVarious();}/* Set the text color @param color A color value in the form 0xAARRGGBB.*/public void setTextColor(@ColorInt int color) {textColor = color;paint.setColor(textColor);}/* set scroll speed @param speed SCROLL SPEED [4,14] / 0?*/public void setSpeed(int speed) {if (speed > 14 || speed < 4) {throw new IllegalArgumentException("Speed was invalid integer, it must between 4 and 14");} else {this.speed = speed;}}/* scroll text forever @param scrollForever scroll forever or not*/public void setScrollForever(boolean scrollForever) {isScrollForever = scrollForever;}public boolean isPauseScroll() {return pauseScroll;}public void setPauseScroll(boolean pauseScroll) {this.pauseScroll = pauseScroll;}@Overridepublic boolean onTouchEvent(MotionEvent event) {if (!clickEnable) {return true;}switch (event.getAction()) {case MotionEvent.ACTION_DOWN:pauseScroll = !pauseScroll;break;}return true;}@Overrideprotected void onVisibilityChanged(View changedView, int visibility) {super.onVisibilityChanged(changedView, visibility);this.setVisibility(visibility);}/* measure text*/private void measureVarious() {textWidth = paint.measureText(text);viewWidth_plus_textLength = viewWidth + textWidth;textX = viewWidth - viewWidth / 5;//baseline measure !FontMetrics fontMetrics = paint.getFontMetrics();float distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;textY = viewHeight / 2 + distance;}//帧速率private static final long FRAME_RATE = 15;private Canvas mCanvas;private void draw(float X, float Y) {long startTime = System.currentTimeMillis();try {//获得canvas对象mCanvas = surfaceHolder.lockCanvas();synchronized (surfaceHolder) {if (mCanvas != null) {mCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);mCanvas.drawText(text, X, Y, paint);}}} catch (Exception e) {e.printStackTrace();} finally {if (mCanvas != null) {//释放canvas对象并提交画布surfaceHolder.unlockCanvasAndPost(mCanvas);}}//需要计算绘制所需的时间,并休眠一段时间以维持一定的帧率long endTime = System.currentTimeMillis();long timeDiff = endTime - startTime;long sleepTime = FRAME_RATE - timeDiff;try {if (sleepTime > 0) {Thread.sleep(sleepTime);}} catch (InterruptedException e) {e.printStackTrace();}}@Overridepublic void run() {measureVarious();while (!stopScroll) {if (pauseScroll) {try {Thread.sleep(500);} catch (InterruptedException e) {Log.e(TAG, e.toString());}continue;}draw(viewWidth - textX, textY);textX += speed;if (textX > viewWidth_plus_textLength) {textX = 0;--needScrollTimes;}if (needScrollTimes <= 0 && isScrollForever) {stopScroll = true;}}}}

具体使用方法请参考源码

源码


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