Why did SurfaceView's onTouchEvent cause a few seconds of delay?

I have a very simple SurfaceView game, and sometimes the game doesn't respond to touch events for a few seconds and then responds to all those touch events at once. I tested my game on Galaxy S3 and Nexus 4 and it works great, it seems this problem only occurs on Galaxy S5.

  • Primary activity:

    public class DroidzActivity extends Activity {
    /** Called when the activity is first created. */
    
    private static final String TAG = DroidzActivity.class.getSimpleName();
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // requesting to turn the title OFF
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        // making it full screen
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        // set our MainGamePanel as the View
        setContentView(new MainGamePanel(this));
        Log.d(TAG, "View added");
    }
    
    @Override
    protected void onDestroy() {
        Log.d(TAG, "Destroying...");
        super.onDestroy();
    }
    
    @Override
    protected void onStop() {
        Log.d(TAG, "Stopping...");
        super.onStop();
    }     
    
    }
    
          

    1. MainGamePanel

Public class MainGamePanel extends SurfaceView implements SurfaceHolder.Callback {

private static final String TAG = MainGamePanel.class.getSimpleName();

private MainThread thread;

public MainGamePanel(Context context) {
    super(context);
    // adding the callback (this) to the surface holder to intercept events
    getHolder().addCallback(this);

    // create the game loop thread
    thread = new MainThread(getHolder(), this);

    // make the GamePanel focusable so it can handle events
    setFocusable(true);
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
        int height) {
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
    // at this point the surface is created and
    // we can safely start the game loop
    thread.setRunning(true);
    thread.start();
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    Log.d(TAG, "Surface is being destroyed");
    // tell the thread to shut down and wait for it to finish
    // this is a clean shutdown
    boolean retry = true;
    while (retry) {
        try {
            thread.setRunning(false);
            thread.join();
            retry = false;
        } catch (InterruptedException e) {
            // try again shutting down the thread
        }
    }
    Log.d(TAG, "Thread was shut down cleanly");
}

public void render(Canvas canvas){
    if(canvas!=null)
        canvas.drawColor(colorList[colorIndex]);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        colorIndex++;
        colorIndex = colorIndex % colorList.length;
    }

    return super.onTouchEvent(event);
}
int [] colorList = {Color.RED, Color.GREEN, Color.BLUE, Color.GRAY};
    int colorIndex = 0;

}

      

  1. MainThread

    The public class MainThread extends the thread {

    private static final String TAG = MainThread.class.getSimpleName();
    
    // Surface holder that can access the physical surface
    private SurfaceHolder surfaceHolder;
    // The actual view that handles inputs
    // and draws to the surface
    private MainGamePanel gamePanel;
    
    // flag to hold game state 
    private boolean running;
    public void setRunning(boolean running) {
        this.running = running;
    }
    
    public MainThread(SurfaceHolder surfaceHolder, MainGamePanel gamePanel) {
        super();
        this.surfaceHolder = surfaceHolder;
        this.gamePanel = gamePanel;
    }
    
    
    // desired fps
    private final static int    MAX_FPS = 50;   
    // maximum number of frames to be skipped
    private final static int    MAX_FRAME_SKIPS = 5;    
    // the frame period
    private final static int    FRAME_PERIOD = 1000 / MAX_FPS;  
    
    
    @Override
    public void run() {
        Canvas canvas;
        Log.d(TAG, "Starting game loop");
    
        long beginTime;     // the time when the cycle begun
        long timeDiff;      // the time it took for the cycle to execute
        int sleepTime;      // ms to sleep (<0 if we're behind)
        int framesSkipped;  // number of frames being skipped 
    
        sleepTime = 0;
    
        while (running) {
            canvas = null;
            // try locking the canvas for exclusive pixel editing
            // in the surface
            try {
                canvas = this.surfaceHolder.lockCanvas();
                synchronized (surfaceHolder) {
                    beginTime = System.currentTimeMillis();
                    framesSkipped = 0;  // resetting the frames skipped
                    // update game state 
                //  this.gamePanel.update();
                    // render state to the screen
                    // draws the canvas on the panel
                    this.gamePanel.render(canvas);              
                    // calculate how long did the cycle take
                    timeDiff = System.currentTimeMillis() - beginTime;
                    // calculate sleep time
                    sleepTime = (int)(FRAME_PERIOD - timeDiff);
    
                    if (sleepTime > 0) {
                        // if sleepTime > 0 we're OK
                        try {
                            // send the thread to sleep for a short period
                            // very useful for battery saving
                            Thread.sleep(sleepTime);    
                        } catch (InterruptedException e) {}
                    }
    
                    while (sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
                        // we need to catch up
                        // update without rendering
                    //  this.gamePanel.update(); 
                        // add frame period to check if in next frame
                        sleepTime += FRAME_PERIOD;  
                        framesSkipped++;
                    }
                }
            } finally {
                // in case of an exception the surface is not left in 
                // an inconsistent state
                if (canvas != null) {
                    surfaceHolder.unlockCanvasAndPost(canvas);
                }
            }   // end finally
        }
    }   
    
          

    }

Here is the simplest version of the app I've tried and I can recreate the same problem again. It also sometimes takes 5-10 seconds to boot on the S5, when it boots under 1 second on the Nexus 4 and S3.

+3


source to share


1 answer


It looks like MainThread is starving on the UI thread.

The code that ends up executing (with a lot of files removed) looks like this:

canvas = this.surfaceHolder.lockCanvas();
// Do a ton of stuff
surfaceHolder.unlockCanvasAndPost(canvas);
canvas = this.surfaceHolder.lockCanvas();
// Do a ton of stuff
surfaceHolder.unlockCanvasAndPost(canvas);
canvas = this.surfaceHolder.lockCanvas();
// Do a ton of stuff
surfaceHolder.unlockCanvasAndPost(canvas);

      

This is supported by the android source. Note what SurfaceHolder#lock

is calling mSurfaceLock.lock()

. It is also called SurfaceHolder#updateWindow

, which is called in various other places in this file.



mSurfaceLock

is ReentrantLock

, and the documentation states:

The constructor of this class takes an optional fairness parameter. When set to true, in a contention setting, locks prefer to grant access to the long awaited thread. Otherwise, this lock does not guarantee a specific order of access.

The SurfaceView does not indicate fairness, so it must use the default, which can lead to exactly this hunger.

Try to move some of your work and in particular sleep outside of the lock / unlock calls.

+5


source







All Articles