仅在特定设备上的 SurfaceView 中出现 ANR——唯一的解决办法是缩短睡眠时间

ANR in SurfaceView on specific devices only -- The only fix is a short sleep time

在我的 Android 应用程序中,我使用 SurfaceView 来绘制东西。它在数千台设备上运行良好——除了现在用户开始在以下设备上报告 ANR:

所以我拿到了LG G4,确实能够验证问题。它与 SurfaceView.

直接相关

现在猜猜是什么在经过数小时的调试后解决了这个问题?它正在取代 ...

mSurfaceHolder.unlockCanvasAndPost(c);

...与...

mSurfaceHolder.unlockCanvasAndPost(c);
System.out.println("123"); // THIS IS THE FIX

怎么会这样?

以下代码是我的渲染线程,除了提到的设备外,它一直运行良好:

import android.graphics.Canvas;
import android.view.SurfaceHolder;

public class MyThread extends Thread {

    private final SurfaceHolder mSurfaceHolder;
    private final MySurfaceView mSurface;
    private volatile boolean mRunning = false;

    public MyThread(SurfaceHolder surfaceHolder, MySurfaceView surface) {
        mSurfaceHolder = surfaceHolder;
        mSurface = surface;
    }

    public void setRunning(boolean run) {
        mRunning = run;
    }

    @Override
    public void run() {
        Canvas c;
        while (mRunning) {
            c = null;
            try {
                c = mSurfaceHolder.lockCanvas();
                if (c != null) {
                    mSurface.doDraw(c);
                }
            }
            finally { // when exception is thrown above we may not leave the surface in an inconsistent state
                if (c != null) {
                    try {
                        mSurfaceHolder.unlockCanvasAndPost(c);
                    }
                    catch (Exception e) { }
                }
            }
        }
    }

}

部分代码来自 Android SDK 中的 LunarLander 示例,更具体地说 LunarView.java

更新代码以匹配来自 Android 6.0(API 级别 23)的改进示例,结果如下:

import android.graphics.Canvas;
import android.view.SurfaceHolder;

public class MyThread extends Thread {

    /** Handle to the surface manager object that we interact with */
    private final SurfaceHolder mSurfaceHolder;
    private final MySurfaceView mSurface;
    /** Used to signal the thread whether it should be running or not */
    private boolean mRunning = false;
    /** Lock for `mRunning` member */
    private final Object mRunningLock = new Object();

    public MyThread(SurfaceHolder surfaceHolder, MySurfaceView surface) {
        mSurfaceHolder = surfaceHolder;
        mSurface = surface;
    }

    /**
     * Used to signal the thread whether it should be running or not
     *
     * @param running `true` to run or `false` to shut down
     */
    public void setRunning(final boolean running) {
        // do not allow modification while any canvas operations are still going on (see `run()`)
        synchronized (mRunningLock) {
            mRunning = running;
        }
    }

    @Override
    public void run() {
        while (mRunning) {
            Canvas c = null;

            try {
                c = mSurfaceHolder.lockCanvas(null);
                synchronized (mSurfaceHolder) {
                    // do not allow flag to be set to `false` until all canvas draw operations are complete
                    synchronized (mRunningLock) {
                        // stop canvas operations if flag has been set to `false`
                        if (mRunning) {
                            mSurface.doDraw(c);
                        }
                    }
                }
            }
            // if an exception is thrown during the above, don't leave the view in an inconsistent state
            finally {
                if (c != null) {
                    mSurfaceHolder.unlockCanvasAndPost(c);
                }
            }
        }
    }

}

但是,这个 class 仍然不适用于上述设备。我出现黑屏并且应用程序停止响应。

(我发现的)解决问题的唯一方法是添加 System.out.println("123") 调用。并在循环结束时添加一个较短的休眠时间,结果提供了相同的结果:

try {
    Thread.sleep(10);
}
catch (InterruptedException e) { }

但这些都不是真正的修复,是吗?是不是很奇怪?

(根据我对代码所做的更改,我还能够在错误日志中看到异常。有很多 developers with the same problem 但不幸的是 none 确实提供了解决方案我的(特定于设备的)案例。

你能帮忙吗?

查看 ANR 跟踪。它似乎卡在哪里? ANR 意味着 main UI 线程没有响应,所以你在渲染器线程上做的事情是无关紧要的,除非两者正在争夺一个锁。

您报告的症状听起来像是一场比赛。如果您的主 UI 线程卡在 mRunningLock 上,可以想象您的渲染器线程只会在很短的时间内 window 保持解锁状态。添加日志消息或睡眠调用使主线程有机会在渲染器线程再次获取主线程之​​前醒来并开始工作。

(这对我来说实际上没有意义——你的代码看起来应该在等待显示刷新时停止等待 lockCanvas()——所以你需要查看ANR.)

FWIW,您不需要在 mSurfaceHolder 上同步。一个早期的例子就是这样做的,从那以后的每个例子都克隆了它。

整理好后,您可能想阅读有关 game loops 的内容。

目前对我有效的是什么,虽然没有真正解决问题的根源,但表面上与症状作斗争:

1。删除 Canvas 操作

我的渲染线程在 SurfaceView 子类上调用自定义方法 doDraw(Canvas canvas)

在该方法中,如果我删除对 Canvas.drawBitmap(...)Canvas.drawRect(...) 的所有调用以及对 Canvas 的其他操作,应用程序将不再冻结。

方法中可能会留下对 Canvas.drawColor(int color) 的单个调用。甚至像 BitmapFactory.decodeResource(Resources res, int id, Options opts) 和 reading/writing 这样昂贵的操作到我的内部 Bitmap 缓存也没有问题。没有冻结。

显然,没有任何绘图,SurfaceView 并没有多大帮助。

2。在渲染线程的 运行 循环

中休眠 10 毫秒

我的渲染线程执行的方法:

@Override
public void run() {
    Canvas c;
    while (mRunning) {
        c = null;
        try {
            c = mSurfaceHolder.lockCanvas();
            if (c != null) {
                mSurface.doDraw(c);
            }
        }
        finally {
            if (c != null) {
                try {
                    mSurfaceHolder.unlockCanvasAndPost(c);
                }
                catch (Exception e) { }
            }
        }
    }
}

只需在循环中添加一个较短的休眠时间(例如在最后)即可修复 LG G4 上的所有冻结问题:

while (mRunning) {
    ...

    try { Thread.sleep(10); } catch (Exception e) { }
}

但谁知道为什么会这样,以及是否真的解决了问题(在所有设备上)。

3。打印一些东西到 System.out

与上述 Thread.sleep(...) 一起使用的同样的事情也适用于 System.out.println("123"),奇怪的是。

4。将渲染线程的启动延迟 10 毫秒

这就是我从 SurfaceView:

中启动渲染线程的方式
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
    mRenderThread = new MyThread(getHolder(), this);
    mRenderThread.setRunning(true);
    mRenderThread.start();
}

将这三行包裹在以下延迟执行中时,应用程序不再冻结:

new Handler().postDelayed(new Runnable() {

    @Override
    public void run() {
        ...
    }

}, 10);

这似乎是因为一开始只有一个可能的死锁。如果它被清除(延迟执行),则没有其他死锁。之后应用程序 运行 就好了。

但是当离开 Activity 时,应用程序再次冻结。

5。只需使用不同的设备

除了 LG G4、索尼 Xperia Z4、华为 Ascend Mate 7、HTC M9(可能还有一些其他设备)之外,该应用程序在数千台设备上运行良好。

这可能是设备特定的故障吗?肯定有人听说过这个......


所有这些 "solutions" 都是 hacky。我希望有更好的解决方案 - 我敢打赌有!

在小米 5 上遇到同样的问题,android 6。Activity canvas 在启动时冻结,并在退出 activity 一段时间后冻结。 使用 lockHardwareCanvas() 而不是 lockCanvas() 解决了这个问题。这个方法直接在android6中不可用,所以我调用了sHolder.getSurface().lockHardwareCanvas();sHolder.getSurface().unlockCanvasAndPost(canvas); 现在无需延迟,正常工作