【Android, is your SurfaceView dormant】

This article has been authorized to publish exclusively on the WeChat public account guolin_blog (郭林)

I have used SurfaceView in my work recently, and found that I don’t have a systematic understanding of SurfaceView, and the online information is also some simple explanations, so here is a summary and I hope it will be helpful to everyone.

Introduction to SurfaceView

The basic definition of SurfaceView has a very detailed description on the Internet, so I won't talk nonsense here. And my simple understanding of it is: the components of the view can be drawn in the sub-thread, while the traditional View is drawn in the UI thread.
I saw such an explanation on the Internet and thought it was good:

SurfaceView is to dig a hole in Window, and it is displayed in this hole, and other Views are displayed on Window, so View can be displayed on SurfaceView, and you can also add some layers on SurfaceView. Traditional View and its derived classes can only be updated on the UI thread, but the UI thread also handles other interactive logic at the same time.

SurfaceView uses

At this time, some friends will ask, when do we use SurfaceView and when do we use traditional custom View?
Generally, we draw a simple view and it takes a short time and does not need to be refreshed frequently. The traditional custom view is enough.
On the contrary, when the view we draw is more complicated and needs to be refreshed frequently, then use SurfaceView. For example: scrolling subtitle effect realization, small games, etc.

basic use

After defining a class that inherits SurfaceView and implements the SurfaceHolder.Callback interface, there are three callback methods, in order:

  • surfaceCreated will be called back every time the interface is visible
  • surfaceChanged Callback every time the view size changes
  • surfaceDestroyed will be called back every time the interface is invisible

The execution order of the 3 normal initialization methods is: surfaceCreated -> surfaceChanged -> surfaceDestroyed
The interface switches to the background and executes: surfaceDestroyed, and executes after returning to the current interface: surfaceCreated -> surfaceChanged
Executes after the screen rotates: surfaceDestroyed -> surfaceCreated -> surfaceChanged
Where SurfaceView is located After the size of the parent control changes, it will execute : surfaceChanged

Let's take drawing a positive selection curve as an example:

Renderings:
Please add a picture description

code show as below:

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);
            }
        }
    }
}

Here are a few places to pay attention to:

  1. When drawing with mCanvas and when releasing the canvas object and submitting the canvas, the mCanvas should be judged empty .
  2. When drawing with mCanvas, first draw a background color mCanvas.drawColor(Color.WHITE);

problem found

At this point, it is estimated that some friends think that there is no problem in writing this way, and there is no problem in copying the code to compile and run. Let's test in this way, frequently switch the background and return to the current drawing interface, I believe the following error will be reported in a few rounds:

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. 

mIsDrawing = false has been executed when the surfaceDestroyed method is executed, the while loop must have stopped, and the drawView method should not be executed. How can such an error be reported?

Preliminary solution to the problem

Friends who know enough about surfaceView should say that when the child thread draws the view, it is necessary to let the thread sleep properly and control the frequency of drawing. That's right, it really needs to be handled this way. We modify it in the drawSomething method, and the code improvement is as follows:

 //帧速率
 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();
        }
    }

After the modification, we compile and run again, and also perform frequent switching background tests. After multiple tests, the error report at the beginning does not appear. Explain that such a change, the effect is good!

thread knowledge

However, after I conducted dozens of tests, I occasionally found that the above error would still be reported. This is why? We are drawing the view in the sub-thread. When we switch the background and return to the current interface, the thread is constantly being created in the surfaceCreated method. Is it caused by the thread not handling it well? With this question in mind, let's review thread knowledge together.
An in-depth understanding of threads is not the focus of this article, let's focus on the key methods. Just like the activity in Android, threads also have a life cycle, mainly in the following stages:

  1. New (new Thread)
  2. Start (start): After calling the start() method of the thread, the thread is waiting for the CPU to allocate resources at this time
  3. Run (run): When the ready thread is scheduled and obtains CPU resources, it enters the running state
  4. Blocked: There are many thread blocking scenarios
  5. Terminated: After the normal execution of the thread is completed or the thread is forcibly terminated in advance or terminated due to an exception, the thread will be destroyed

Thread blocked (blocked)

There are many thread blocking scenarios:

  1. Waiting for I/O stream input and output
  2. network request
  3. Call the sleep() method, and the blocking will stop after the sleep time ends
  4. After calling the wait() method and calling notify() to wake up the thread, the blocking stops
  5. When other threads execute the join() method, the current thread will be blocked, and it needs to wait for other threads to finish executing.

The key method we want to talk about is here, it is it, it is it, join . According to the characteristics of this method, we execute the thread in the surfaceDestroyed method

mThread.join(); 

What will it do? It stands to reason that when mThread calls the join method, the current thread, that is, the UI thread, will be blocked (as for why the current thread is a UI thread, I will explain it later), and it will stop blocking after waiting for the mThread thread to finish executing.
How to prove it? Let's simply modify the code. First, after the mThread.join() method is executed, a line of log is printed.
insert image description here
Then we extend the execution time of the child thread:
insert image description here

Finally compile and run the project, initialize the interface and then switch the app to the background, the log is printed as follows:

insert image description here
We only need to focus on 2 places where the log is printed:

  1. From the log printing, we know that the current UI thread is 3055, and the thread ids of surfaceCreated, surfaceChanged, and surfaceDestroyed are also 3055, and they are all on the UI thread, which is why it is said that the current thread is the UI thread. And the child thread id of our drawing view is 3086.
  2. After the mThread.join() method is executed in the surfaceDestroyed method, the UI thread is indeed blocked for nearly 20 seconds, and then the subsequent log printing continues.

Summary: Through the above methods, we have proved that after executing the mThread.join() method, the current thread, that is, the UI thread, will be blocked, and the blocking will stop after the mThread thread is executed.

finally solve the problem

With the understanding of the thread's join method, we can improve the code in the surfaceDestroyed method as follows:

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

    }

After such improvements, no matter how frequently we switch between the front and back, I believe we won’t be reporting errors! Our code quality will also be better, but is the code quality currently the best? If you have read this article by Mr. Guo, what is the use of the volatile keyword in Android? You will find that the current code is not perfect. In fact, we'd better modify the mIsDrawing variable with volatile, like this:

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

As for why the volatile keyword is added, Mr. Guo explained it in detail in the article. Interested friends should check it out. Even if our problem is solved here, the complete code is as follows:

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);
            }
        }
    }


}

Text scrolling effect based on surfaceView

First look at the effect:
Please add a picture description

Due to space reasons, the specific code will not be displayed here, please click here .

If you think the article is helpful to you, please give it a like, thank you very much!

source code


Refer to the article
for a detailed explanation of the join() method in Java threads
The principle and usage tutorial of the join() method in Java

Guess you like

Origin blog.csdn.net/da_caoyuan/article/details/130090833