Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Canvas restore() causing underflow exception in very rare cases

Through ACRA, I have received a small number of reports from an alpha build of software that show that an exception is occurring during a specific call to Canvas.restore(). The exception is java.lang.IllegalStateException: Underflow in restore.

I am well aware that this exception shall occur if one too many restore() calls are made than save() calls. However, from very careful code inspection, I am absolutely certain that all calls to canvas.save() and canvas.restore() are balanced. That is, all calls to canvas.save() are most certainly balanced at a later stage by a call to canvas.restore(). I can also confirm that there are no conditionals, exceptions or early returns from methods that could result in a missing canvas.save() leading to the stack underflow.

Furthermore, this issue seems to be a rare edge-case that is resulting in the exception happening only a handful of times within graphics code that renders many times every second.

The sort of structure of the code in which this is happening is:

protected void onDraw(Canvas canvas) {
    ...
    someMethod(canvas);
    ...
}

void someMethod(Canvas canvas)
{
    ....
    canvas.save();
    ....
    someOtherMethod(Canvas canvas);
    ....
    canvas.restore();  
    ....
}

void someOtherMethod(Canvas canvas)
{

    ....
    canvas.save();
    ....
    for ( ... ) {

        ....
        canvas.save();
        ...
        canvas.restore();
        ...
    }
    ....
    canvas.restore();   // *** exception here ***   
    ....
}

There are places where save() / restore() is used within a loop, but again the calls are balanced.

The reports are from devices running 4.3 and 4.4.2.

I have attempted to Google this issue and the only interesting QA I could find is from a person who clearly had unbalanced save() / restore() calls in his code; that is categorically not the issue in my case.

This is occurring within the call stack of a custom View subclass' onDraw(Canvas canvas) method, all happening on the UI thread. There are no other threads I have touching that Canvas object.

I could potentially check the stack with a call to getSaveCount() before invoking the specific restore() that has been responsible for the exceptions, but that would really be just sticking an Elastoplast over the problem. I'd rather understand what on earth the edge case is that's causing the underflow, but it is baffling me.

Are there any known issues? Could it be possible for any kind of system configuration change to affect this Canvas while the UI thread is within the context of a View's onDraw() call? Are there any known limitations on the Canvas stack size? Could there be any OS graphics calls that are known to misuse the canvas stack?

like image 713
Trevor Avatar asked May 27 '14 15:05

Trevor


1 Answers

This isn't an answer that explains the root cause, as sadly I still don't know the cause. What I am presenting here is the solution that has allowed me to cope with it.

Very simply, the whole rendering process is surrounded with a try / catch as follows:

        try {
            ...
            // calls go within here to render to the canvas many times
            ...

        }
        catch (java.lang.IllegalStateException exception) {
            // Attempt to catch rare mysterious Canvas stack underflow events that have been reported in
            // ACRA, but simply should not be happening because Canvas save()/restore() calls are definitely
            // balanced. The exception is: java.lang.IllegalStateException: Underflow in restore
            // See: stackoverflow.com/questions/23893813/
            if (exception.getMessage() != null && (//
                    exception.getMessage().contains("Underflow in restore") || //
                    exception.getCause().getMessage().contains("Underflow in restore"))) { //
                DebugLog.getInstance().e("Caught a Canvas stack underflow! (java.lang.IllegalStateException: Underflow in restore)");
            }
            else {
                // It wasn't a Canvas underflow, so re-throw.
                throw exception;
            }
        }

Note that the DebugLog.getInstance().e is my own SD card debug logger class which I'm not detailing here; you'd replace that call with whatever you want to log the exception.

A few users' devices have since sent me ACRA reports relating to completely different issues and I have noticed that some of those reports contain attached logs that show that these Canvas exceptions have been caught. This seems to be from Samsung Galaxy devices. Judging from the logs, the device threw this exception maybe two or three times, but then the user has been able to carry on using the application as normal. So it seems that there is no long-lasting negative effect of catching and essentially masking this exception.

like image 153
Trevor Avatar answered Oct 23 '22 02:10

Trevor