How I Fix Error Restarting Game Activity Due to an Locking the Canva

When I started developing a 2D Android game using SurfaceView and a custom MainThread, everything seemed to work smoothly animations, frame rates, rendering all was perfect. Until I hit a critical issue that had me scratching my head.

Every time I tried to restart my game activity using startActivity(intent), the screen would go black and I’d be bombarded with a wall of canvas-related errors in the logs.

Here’s how I unraveled the problem, fixed it, and even added some new functionality like an FPS counter and pause/resume behavior.

Error Breakdown

Here’s what I was seeing in my logcat output:

IllegalArgumentException: canvas object must be the same instance...
Surface was already locked
NullPointerException in draw(Canvas)

These are the three key errors that broke my game:

Illegal Argument Exception

This error means I was calling:

surfaceHolder.unlockCanvasAndPost(canvas);

with a canvas object that was either null or no longer valid. That’s a big no-no in rendering threads.

Surface was already locked

My thread was trying to lockCanvas() again before unlocking the previous one. This caused a deadlock-like state.

Null Pointer Exception in draw

When the lockCanvas() fails and returns null, my drawing logic still went ahead and tried to use the canvas—boom, NullPointerException.

Root Cause

After digging into Android’s SurfaceView lifecycle, I realized the issue boiled down to improper surface/thread handling when the activity was restarted.

  • The SurfaceView was in a transitional state.
  • My rendering Thread didn’t stop fast enough.
  • It tried to draw to a surface that was already destroyed or being recreated.

This inconsistency broke everything.

How I Fix the Canva Lock Error

Here’s how I tackled the issue and cleaned up my code.

Check if Surface is Valid

Before trying to draw anything, I now check:

if (!surfaceHolder.getSurface().isValid()) continue;

This ensures I don’t try to lock or draw on an invalid surface.

Properly Stop the Thread Before Restarting

I updated my close() method to guarantee the thread exits cleanly:

public static void close(String caller) {
Log.d(TAG + ":close()", caller);
thread.setRunning(false);
boolean retry = true;

while (retry) {
try {
thread.join();
retry = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}

Log.d(TAG, "Thread closed.");
}

This prevents multiple threads from trying to render to the same surface.

Guard the Drawing Logic

I now ensure that I only attempt to draw if the canvas is not null. Sounds simple, but this saved me from a lot of NullPointerExceptions.

Improve Run Loop with Safety Check

This is how my updated run() method looks:

@Override
public void run() {
long starttime;
long timeMillis;
long waitTime;
int frameCount = 0;
long totalTime = 0;
long targetTime = 1000 / MAX_FPS;

while (running) {
if (!surfaceHolder.getSurface().isValid()) {
continue;
}

Canvas canvas = null;
starttime = System.nanoTime();
long millitime = starttime / 1_000_000;

try {
canvas = surfaceHolder.lockCanvas();

if (canvas != null) {
synchronized (surfaceHolder) {
gameView.update(millitime);
gameView.draw(canvas);
}
}

} catch (Exception e) {
e.printStackTrace();
} finally {
if (canvas != null) {
try {
surfaceHolder.unlockCanvasAndPost(canvas);
} catch (Exception e) {
e.printStackTrace();
}
}
}

timeMillis = (System.nanoTime() - starttime) / 1_000_000;
waitTime = targetTime - timeMillis;

try {
if (waitTime > 0) {
sleep(waitTime);
}
} catch (Exception e) {
e.printStackTrace();
}

totalTime += System.nanoTime() - starttime;
frameCount++;

if (frameCount == MAX_FPS) {
avrageFPS = (short) (1000 / ((totalTime / frameCount) / 1_000_000));
gameView.setFPS(avrageFPS); // Send it to GameView
frameCount = 0;
totalTime = 0;
}
}
}

Practice Functionality I Added

FPS Counter

This helps me monitor performance in real-time.

In GameView:

private Paint fpsPaint = new Paint();
private int fps = 0;

public GameView(Context context) {
super(context);
fpsPaint.setColor(Color.WHITE);
fpsPaint.setTextSize(40);
fpsPaint.setAntiAlias(true);
}

public void setFPS(int fps) {
this.fps = fps;
}

@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.drawText("FPS: " + fps, 20, 40, fpsPaint);
}

In MainThread, I send FPS to the view like this:

gameView.setFPS(avrageFPS);

Pause and Resume Handling

To make sure the thread doesn’t keep running when the activity is paused or resumed, I added:

@Override
protected void onPause() {
super.onPause();
thread.setRunning(false);
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
protected void onResume() {
super.onResume();
thread = new MainThread(getHolder(), this);
thread.setRunning(true);
thread.start();
}

This made my game more stable and responsive to lifecycle events.

Final Thought

This bug taught me something important never take SurfaceView lifecycle for granted. Threads and surfaces are tightly coupled and they need to be handled gracefully, especially during activity restarts.

Related blog posts