Android, is your SurfaceView dormant

This article has authorized the WeChat public account guolin_blog (Guo Lin) to publish exclusively

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 complex and needs to be refreshed frequently, then use SurfaceView. For example: scrolling subtitle effect realization, small games, etc.

Basic usage

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 sequence of the three 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
Execute after the screen rotates: surfaceDestroyed -> surfaceCreated -> surfaceChanged
It will be executed after the size of the parent control where the SurfaceView is located changes: 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;

/**
 * Draw positive selection curve
 */
public class SurfaceViewSinFun extends SurfaceView implements SurfaceHolder.Callback, Runnable {<!-- -->
    private static final String TAG = "SurfaceViewSinFun";
    private Thread mThread;
    private SurfaceHolder mSurfaceHolder;
    //Drawing Canvas
    private Canvas mCanvas;
    // sub thread flag bit
    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();
        //path starting point (0, 100)
        mPath. moveTo(0, 100);
        initView();
    }

    /**
     * Initialize 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);
            // add new coordinates
            mPath.lineTo(x, y);
        }
    }

    private void drawSomething() {<!-- -->
        drawView();
    }
    private void drawView() {<!-- -->
        try {<!-- -->
            //Get a Canvas object,
            mCanvas = mSurfaceHolder. lockCanvas();
            synchronized (mSurfaceHolder) {<!-- -->
                if (mCanvas != null) {<!-- -->
                    // draw the background
                    mCanvas.drawColor(Color.WHITE);
                    // draw the path
                    mCanvas.drawPath(mPath, mPaint);
                }
            }
        } catch (Exception e) {<!-- -->
            e.printStackTrace();
        } finally {<!-- -->
            if (mCanvas != null) {<!-- -->
                //Release the canvas object and submit the canvas
                mSurfaceHolder. unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

Here are a few places to pay attention to:

  1. When using mCanvas to draw and when releasing the canvas object and submitting the canvas, you must make mCanvas 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)
or
java.lang.IllegalStateException: Surface has already been lockedCanvas.

When executing the surfaceDestroyed method, mIsDrawing = false, the while loop must have stopped, and the drawView method should not be executing. How can such an error be reported?

Initial problem solving

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:

 //frame rate
 private static final long FRAME_RATE = 30;
 private void drawSomething() {<!-- -->
        long startTime = System. currentTimeMillis();
        drawView();

        //Need to calculate the time required for drawing, and sleep for a period of time to maintain a certain frame rate
        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. If other threads execute the join() method, the current thread will be blocked and need 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 print a line of log after the mThread.join() method is executed

Then we extend the execution time of the child thread:

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


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: ");
        // end thread
        boolean retry = true;
        while (retry) {<!-- -->
            try {<!-- -->
                mIsDrawing = false;
                mThread. join();
                retry = false;
            } catch (InterruptedException e) {<!-- -->
                //e.printStackTrace();
                // If the thread cannot end normally, continue to retry
            }
        }

    }

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:

 //Sub-thread flag bit
    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;

/**
 * Usage of standard SurfaceView
 * Draw positive selection curve
 */
public class SurfaceViewSinFun extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    private static final String TAG = "SurfaceViewSinFun";

    //Frame rate
    private static final long FRAME_RATE = 30;

    private Thread mThread;
    private SurfaceHolder mSurfaceHolder;
    //Drawing Canvas
    private Canvas mCanvas;
    // sub thread flag bit
    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();
        //path starting point (0, 100)
        mPath. moveTo(0, 100);
        initView();
    }

    /**
     * Initialize 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: ");
        // end thread
        boolean retry = true;
        while (retry) {<!-- -->
            try {<!-- -->
                mIsDrawing = false;
                mThread. join();
                retry = false;
            } catch (InterruptedException e) {<!-- -->
                //e.printStackTrace();
                // If the thread cannot end normally, continue to retry
            }
        }

    }

    @Override
    public void run() {
        while (mIsDrawing) {
            drawSomething();
            x + = 1;
            y = (int) (100 * Math. sin(2 * x * Math. PI / 180) + 400);
            // add new coordinates
            mPath.lineTo(x, y);
        }
    }

    /**
     * Core method 1:
     *
     * Use the lockCanvas() method of SurfaceHolder to get a Canvas object,
     * And draw the game interface in the synchronization block, and finally use the unlockCanvasAndPost() method of SurfaceHolder to release the Canvas object and submit the drawing result.
     * After the drawing is completed, we need to calculate the time required for drawing and sleep for a period of time to maintain a certain frame rate.
     */
    private void drawSomething() {
        long startTime = System. currentTimeMillis();
        drawView();

        //Need to calculate the time required for drawing, and sleep for a period of time to maintain a certain frame rate
        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();
        }
    }

    /**
     * Core Method 2
     */
    private void drawView() {
        try {
            //Get a Canvas object,
            mCanvas = mSurfaceHolder. lockCanvas();
            synchronized (mSurfaceHolder) {
                if (mCanvas != null) {
                    // draw the background
                    mCanvas.drawColor(Color.WHITE);
                    // draw the path
                    mCanvas.drawPath(mPath, mPaint);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (mCanvas != null) {
                //Release the canvas object and submit the canvas
                mSurfaceHolder. unlockCanvasAndPost(mCanvas);
            }
        }
    }


}

Based on surfaceView text scrolling effect

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 are interested.

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

Source code

reference article
Explain the join() method in Java thread in detail
The principle and usage tutorial of the join() method in Java