【问题标题】:ANR in SurfaceView on specific devices only -- The only fix is a short sleep time仅在特定设备上的 SurfaceView 中的 ANR - 唯一的解决方法是睡眠时间短
【发布时间】:2016-03-22 05:31:27
【问题描述】:

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

  • LG G4
    • Android 5.1
    • 3 GB 内存
    • 5.5 英寸显示屏
    • 2560 x 1440 像素分辨率
  • 索尼 Xperia Z4
    • Android 5.0
    • 3 GB 内存
    • 5,2" 显示屏
    • 1920 x 1080 像素分辨率
  • 华为Ascend Mate 7
    • Android 5.1
    • 3 GB 内存
    • 6.0" 显示屏
    • 1920 x 1080 像素分辨率
  • 宏达 M9
    • Android 5.1
    • 3 GB 内存
    • 5.0" 显示屏
    • 1920 x 1080 像素分辨率

所以我得到了一台 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);
                }
            }
        }
    }

}

但是,这个类仍然不能在提到的设备上工作。我出现黑屏并且应用程序停止响应。

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

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

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

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

你能帮忙吗?

【问题讨论】:

  • surfaceCreated() 创建线程应该可以正常工作。参见例如github.com/google/grafika/blob/master/src/com/android/grafika/… 。有关 SurfaceView 和 Activity 交互的一些额外说明,请参阅source.android.com/devices/graphics/architecture.html#activity
  • 谢谢!是的,我已经阅读了所有这些内容。这就是为什么我可以消除大部分可能的原因。一定是一些奇怪的竞争条件或线程锁定问题,在所有其他设备上都不会发生。我认为这可能是这些设备的高显示分辨率。但在平板电脑上,这些高分辨率已经很普遍了,而且它也适用于具有高分辨率的 Nexus 7 (2013) 等设备。但是,如果这有所作为,那么可能是更高的像素密度。也许是因为我在xxhdpixxxhdpi 中没有任何使用过的drawable。
  • 我的渲染线程的run() 循环在后台连续执行,调用的doDraw() 方法大约每40ms 运行一次。因此,似乎确实没有任何事情运行太久或让Canvas 忙碌太久。尽管如此,主线程还是冻结了,屏幕是黑色的。

标签: java android surfaceview android-anr-dialog


【解决方案1】:

目前对我有用的方法,虽然没有真正解决问题的原因,但从表面上解决了症状:

1。删除Canvas 操作

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

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

Canvas.drawColor(int color) 的单个调用可能会留在方法中。甚至像BitmapFactory.decodeResource(Resources res, int id, Options opts) 和读取/写入我的内部Bitmap 缓存这样的昂贵操作也很好。没有冻结。

显然,没有任何绘图,SurfaceView 并没有真正的帮助。

2。在渲染线程的运行循环中休眠 10ms

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

@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。延迟渲染线程的启动 10ms

这就是我从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、Sony Xperia Z4、Huawei Ascend Mate 7、HTC M9(可能还有其他一些设备)之外,该应用在数千台设备上都能正常运行。

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


所有这些“解决方案”都是 hacky。我希望有更好的解决方案——我敢打赌!

【讨论】:

    【解决方案2】:

    查看 ANR 跟踪。它似乎卡在哪里? ANR 意味着 主 UI 线程无法响应,因此除非两者争夺锁,否则您在渲染器线程上所做的事情是无关紧要的。

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

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

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

    一旦你解决了这个问题,你可能想了解一下game loops

    【讨论】:

    • 非常感谢!您关于 Android 图形架构的文档非常有趣。当应用程序冻结时,我的主线程卡在java.lang.Object.wait(Native Method), java.lang.Thread.parkFor(Thread.java:1220), ..., java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:256), android.view.SurfaceView.updateWindow(SurfaceView.java:521), android.view.SurfaceView$3.onPreDraw(SurfaceView.java:176) 正如您在我的第一个示例中看到的那样,我最初并没有在mSurfaceHolder 上同步,但是 latest “Lunar Lander”示例做到了.请在问题末尾查看我的编辑。
    • android.googlesource.com/platform/frameworks/base/+/… 处的代码看起来与您的跟踪很接近。它似乎卡在mSurfaceLock.lock();当 Canvas 被锁定时,该锁也会被持有。因此,您的渲染器线程似乎将 Canvas 锁定了很长一段时间。
    • 谢谢,看起来真的很接近。但这怎么可能是我的代码中的错误,例如我的线程将Canvas 锁定太久? (1) 我已更改我的代码以匹配“月球着陆器”示例,但它仍然无法正常工作。 (2) 它适用于数千台设备,但不适用于这几台(可能几十台)。 (3) 如前所述,当我延迟启动线程时,除了关闭时,其他问题都没有了。这三点可以说明这些设备的故障,对吧?但我没有发现任何关于这些设备问题的信息,而且肯定有人听说过。
    • 另一方面,当我将doDraw() 方法减少为仅绘制黑色背景时,问题就消失了。所以它真的可能是doDraw() 方法花费了太长时间,然后Canvas 被锁定了太久。但是为什么这不会发生在所有其他(旧)设备上呢?唯一的原因可能是高分辨率?再说一次,我试图通过“setFixedSize()”来“减少”它。没用。此外,while (mRunning) { } 循环末尾的简单Thread.sleep(10); 似乎也解决了这个问题。但它很老套,我只能在一台设备上验证这一点,即 LG G4。
    • 您可以在循环的开始和结束处添加一些System.nanoTime() 调用,并观察锁定的持有时间。也许跟踪最大持续时间并每 3 秒记录一次(然后重置最大值)以避免日志泛滥。更好的方法是使用 android.os.Trace 添加自定义 systrace 事件,并在其处于不愉快状态时捕获跟踪 - 参见例如bigflake.com/systrace 。这将为您提供各种线程及其交互的良好视觉表示。
    【解决方案3】:

    在 xiaomi mi 5, android 6 上遇到同样的问题。画布在启动时冻结的 Activity 和退出此 Activity 后的一段时间。 使用lockHardwareCanvas() 而不是lockCanvas() 解决了这个问题。此方法在android 6 中无法直接使用,因此我调用了sHolder.getSurface().lockHardwareCanvas();sHolder.getSurface().unlockCanvasAndPost(canvas); 现在不需要延迟,工作正常

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-06
      • 2014-06-28
      • 1970-01-01
      • 1970-01-01
      • 2016-06-14
      • 2011-06-22
      • 2011-12-22
      相关资源
      最近更新 更多