SurfaceView appears ANR: Surface has already been released solution

There is such a scene in the project that will cause ANR on SurfaceView. Create and use SurfaceView in the main Activity, and then enter the sub-ActivityB continuously, return to the main Activity and then enter the sub-ActivityB. In this cycle, ANR problems will occur.

I found a pit by checking the source code of SurfaceView. In fact, many people use the wrong posture. It is just luck that they don’t have ANR.

1. How to find ANR logs

After ANR appeared, I immediately thought of getting the ANR log, which can be obtained through the following command:

adb pull data/anr/traces.txt

This will download the ANR log to the computer.

2. Analyze ANR logs

Open the ANR log, you can see the stack information of the main thread

“main” prio=5 tid=1 Waiting
| group=”main” sCount=1 dsCount=0 obj=0x75372268 self=0xaabd3ce0
| sysTid=3980 nice=-1 cgrp=top_visible sched=0/0 handle=0xf7762b50
| state=S schedstat=( 22407689743 17485653812 44070 ) utm=1882 stm=358 core=5 HZ=100
| stack=0xff307000-0xff309000 stackSize=8MB
| held mutexes=
at java.lang.Object.wait!(Native method)
– waiting on <0x0488bcf7> (a java.lang.Object)
at java.lang.Thread.parkFor$(Thread.java:1235)
– locked <0x0488bcf7> (a java.lang.Object)
at sun.misc.Unsafe.park(Unsafe.java:299)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:158)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:810)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:843)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1172)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:181)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:257)
at android.view.SurfaceView.updateWindow(SurfaceView.java:517)
at android.view.SurfaceView.onWindowVisibilityChanged(SurfaceView.java:246)
at android.view.View.dispatchWindowVisibilityChanged(View.java:9737)
at android.view.ViewGroup.dispatchWindowVisibilityChanged(ViewGroup.java:1309)
at android.view.ViewGroup.dispatchWindowVisibilityChanged(ViewGroup.java:1309)
at android.view.ViewGroup.dispatchWindowVisibilityChanged(ViewGroup.java:1309)
…repeated 10 times
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1415)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1139)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6238)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:884)
at android.view.Choreographer.doCallbacks(Choreographer.java:696)
at android.view.Choreographer.doFrame(Choreographer.java:631)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:870)
at android.os.Handler.handleCallback(Handler.java:743)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:150)
at android.app.ActivityThread.main(ActivityThread.java:5621)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:794)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:684)

The most direct log should be the main thread waiting problem caused by SurfaceView
at java.lang.Object.wait!(Native method)
at android.view.SurfaceView.updateWindow(SurfaceView.java:517)

1. Analyze the SurfaceView source code:

According to the log, it can be seen that the ANR caused by SurfacecView, the main interface uses SurfaceView, and it has been blocked here when executing ReentrantLock.lock() in the SurfaceView.updateWindow method, resulting in ANR.

Open the SurfaceView source code and see the mSurfaceLock.lock() method in the updateWindow method.

mSurfaceLock is defined like this: final ReentrantLock mSurfaceLock = new ReentrantLock();

There must be a place where unlock is not called to release the lock, so the lock cannot be obtained when the lock is called. It is thought that Canvas has a lock, and the developer needs to unlock it in time.

There is no problem with the code for manipulating the canvas, and unlocking in finally is also correct, as follows:

Canvas canvas = mHolder. lockCanvas();

    if(canvas != null){

        try {

            for (Heart heart : mHeartArray) {

                canvas.drawBitmap(heart.bitmap, null, heart.dst, mPaint);

            }

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            mHolder.unlockCanvasAndPost(canvas);

        }

    }

I repeatedly switch the front and back of the Activity, because the SurfaceView will be destroyed when it is invisible, and it will be created when it is visible. At this time, ANR was finally reproduced, and an exception was seen:

System.err: java.lang.IllegalStateException: Surface has already been released.

So I started to analyze the source code in detail, first look at the implementation of unlockCanvasAndPost, because it is possible to unlock

 // Implementation of SurfaceView.SurfaceHolder

    @Override

    public void unlockCanvasAndPost(Canvas canvas) {

        mSurface.unlockCanvasAndPost(canvas);

        mSurfaceLock. unlock();

    }

    // Surface class

    public void unlockCanvasAndPost(Canvas canvas) {

        synchronized(mLock) {

            checkNotReleasedLocked();

            //...

        }

    }

// Found the location where the exception was thrown. If an exception is thrown here, then SurfaceLock.unlock will not be executed, and finally ANR will appear when it is locked again.

// When mNativeObject=0, this exception will be thrown, and then see how mNativeObject is reset to 0.

 private void checkNotReleasedLocked() {

        if (mNativeObject == 0) {

            throw new IllegalStateException("Surface has already been released.");

        }

    }

// Originally this method will set mNativeObject to 0, then analyze where to call this method

 private void setNativeObjectLocked(long ptr) {

        //...

        mNativeObject = ptr;

        //...

    }

// After searching, it turns out that setNativeObjectLocked(0) is called here

@Deprecated

    public void transferFrom(Surface other) {

        if (other != this) {

            //...

            other.setNativeObjectLocked(0);

            //...

        }

    }

// call transferFrom in SurfaceView

 /** @hide */

    protected void updateWindow(boolean force, boolean redrawNeeded) {

        mSurfaceLock. lock();

        try {

        } finally {

            mSurfaceLock. unlock();

        }

        try {

            ....

            if (mSurfaceCreated & amp; & amp; (surfaceChanged || (!visible & amp; & amp; visibleChanged))) {

                mSurfaceCreated = false;

                if (mSurface. isValid()) {

                    callbacks = getSurfaceCallbacks();

                    for (SurfaceHolder. Callback c : callbacks) {

                        c.surfaceDestroyed(mSurfaceHolder);

                    }

                }

            }

            mSurface. transferFrom(mNewSurface);

            ....

        } finally {

        }

    }

}

SurfaceView life cycle is as follows:

surfaceCreated: When changing from invisible to visible

surfaceChanged: when the size changes

surfaceDestroyed: When changing from a visible state to an invisible state

According to the BUG reproduction steps, click the setting button to jump to the subpage. At this time, the main interface is invisible, so the SurfaceView will be destroyed, so surfaceDestroyed will be called.

// As can be seen from the above code, first call back surfaceDestroyed, and then execute mSurface.transferFrom(mNewSurface), then set mNativeObject to 0,

// If unlockCanvasAndPost is called at this time, an exception will be thrown, and unlock cannot be called, resulting in ANR the next time SurfaceView is created.

The reason for ANR: In short, when between lockCanvas and unlockCanvasAndPost, SurfaceView is destroyed, which causes unlock to fail and deadlock occurs.

Summarize this ANR process:

Step 1: Execute mHolder.lockCanvas(), and the lock is successfully acquired

Step 2: At this time, it happens that SurfaceView is destroyed, surfaceDestroyed is executed, and mNativeObject is set to 0

Step 3: Call unlockCanvasAndPost, but because mNativeObject is 0, an exception is thrown, and the unlock is not successful

Step 4: Recreate SurfaceView and try to lock. Because the last lock was not released, it entered an infinite wait.

Solution: 2 steps

1. Add a synchronization lock in the process of operating the canvas, so that the entire process of operating the canvas as a whole

synchronized (this) {
if (mDrawFlag) {
Canvas canvas = mHolder. lockCanvas();
if (canvas != null) {
try {
for (Heart heart : mHeartArray) {
canvas.drawBitmap(heart.bitmap, null, heart.dst, mPaint);
}
  
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
mHolder.unlockCanvasAndPost(canvas);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

2. Add a synchronization lock in the SurfaceView destruction callback to ensure that mNativeObject will not be set to 0 between lockCanvas and unlockCanvasAndPost

@Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        synchronized (this) {
            mDrawFlag = false;
        }
    }