Подтвердить что ты не робот

ANR в SurfaceView только для определенных устройств - единственное исправление - короткое время сна

В моем приложении для Android я использую SurfaceView для рисования вещей. Он отлично работает на тысячах устройств, за исключением того, что теперь пользователи начали сообщать о ANR на следующих устройствах:

  • LG G4
    • Android 5.1
    • Оперативная память 3 ГБ
    • 5.5 "
    • Разрешение 2560 x 1440 пикселей
  • Sony Xperia Z4
    • Android 5.0
    • Оперативная память 3 ГБ
    • 5,2 "
    • Разрешение 1920 x 1080 пикселей
  • Huawei Ascend Mate 7
    • Android 5.1
    • Оперативная память 3 ГБ
    • 6.0 "
    • Разрешение 1920 x 1080 пикселей
  • HTC M9
    • Android 5.1
    • Оперативная память 3 ГБ
    • 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) { }
                }
            }
        }
    }

}

Код, по частям, из примера LunarLander в Android SDK, более конкретно 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 с та же проблема, но, к сожалению, никто не предоставляет решение для моего (конкретного устройства) случая.

Вы можете помочь?

4b9b3361

Ответ 1

Что в настоящее время работает для меня, хотя на самом деле не фиксирует причину проблемы, но поверхностно нарушает симптомы:

1. Удаление операций Canvas

Мой поток визуализации вызывает пользовательский метод doDraw(Canvas canvas) в подклассе SurfaceView.

В этом методе, если я удалю все вызовы Canvas.drawBitmap(...), Canvas.drawRect(...) и другие операции с Canvas, приложение больше не замерзает.

В методе может остаться один вызов Canvas.drawColor(int color). И даже дорогостоящие операции типа BitmapFactory.decodeResource(Resources res, int id, Options opts) и чтение/запись в мой внутренний кеш 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, Sony Xperia Z4, Huawei Ascend Mate 7, HTC M9 (и, возможно, нескольких других устройств), приложение отлично работает на тысячах устройств.

Может ли это быть глюкометр, специфичный для устройства? Можно было бы услышать об этом...


Все эти "решения" хаки. Я хочу, чтобы было лучшее решение - и я уверен, что есть!

Ответ 2

Посмотрите на трассировку ANR. Где он, кажется, застревает? ANR означают, что основной поток пользовательского интерфейса не отвечает, поэтому то, что вы делаете в потоке рендеринга, не имеет значения, если они не сражаются за блокировку.

Симптомы, о которых вы сообщаете, звучат как гонка. Если ваш основной поток пользовательского интерфейса застрял, скажем, mRunningLock, можно предположить, что поток вашего рендеринга оставит его незаблокированным для очень короткого окна. Добавление сообщения журнала или вызова сон дает основному потоку возможность проснуться и работать до того, как поток рендеринга снова захватит его.

(Это на самом деле не имеет смысла для меня - ваш код выглядит так, что он должен быть остановлен, ожидая lockCanvas(), ожидая обновления дисплея, поэтому вам нужно посмотреть на трассировку потока в ANR.)

FWIW, вам не нужно синхронизировать с mSurfaceHolder. Ранний пример сделал это, и каждый пример с тех пор клонировал его.

Как только вы получите это, вы можете прочитать о игровых циклах.