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) { }

Но это не настоящие исправления, не так ли? Разве это не странно?

(В зависимости от того, какие изменения я внес в код, я также могу увидеть исключение в журнале ошибок. Существует много разработчики с то же самое проблема, но, к сожалению, ни один из них не дает решения для моего (зависящего от устройства) случая.

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


person caw    schedule 16.12.2015    source источник
comment
Создание потока из surfaceCreated() должно работать нормально. См., например. github.com/google/grafika/blob/ master/src/com/android/grafika/ . Некоторые дополнительные примечания о взаимодействии SurfaceView и Activity см. в разделе source.android.com/devices. /graphics/architecture.html#activity   -  person fadden    schedule 17.12.2015
comment
Спасибо! Да, я все это уже прочитал. Вот почему я могу устранить большинство возможных причин. Должно быть какое-то странное состояние гонки или проблема с блокировкой потока, которая не возникает на всех других устройствах. Я думал, что это может быть высокое разрешение экрана этих устройств. Но на планшетах эти высокие разрешения уже были обычным явлением, и они работают на таких устройствах, как Nexus 7 (2013 г.), который также имеет высокое разрешение. Однако тогда может быть более высокая плотность пикселей, если это имеет значение. Может быть, потому что у меня нет ни одного из используемых рисунков в xxhdpi или xxxhdpi.   -  person caw    schedule 17.12.2015
comment
Цикл run() моего потока рендеринга постоянно выполняется в фоновом режиме, а вызываемый метод doDraw() запускается примерно каждые 40 мс. Так что на самом деле не похоже, чтобы что-то работало слишком долго или слишком долго занимало Canvas. Все равно основной поток зависает и экран черный.   -  person caw    schedule 17.12.2015


Ответы (3)


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

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 (и, вероятно, нескольких других устройств), приложение отлично работает на тысячах устройств.

Может ли это быть глюк конкретного устройства? Об этом наверняка слышали...


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

person caw    schedule 19.12.2015

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

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

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

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

Разобравшись с этим, вы можете прочитать об игровых циклах. .

person fadden    schedule 16.12.2015
comment
Большое спасибо! Ваш документ о графической архитектуре 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, но в последнем примере Lunar Lander это произошло. Пожалуйста, смотрите мое редактирование в конце вопроса. - person caw; 17.12.2015
comment
Код на android .googlesource.com/platform/frameworks/base/+/ выглядит близко к вашей трассировке. Кажется, он застревает на mSurfaceLock.lock(); эта блокировка также удерживается, пока холст заблокирован. Таким образом, похоже, что ваш поток рендерера держит Canvas заблокированным в течение длительного периода. - person fadden; 17.12.2015
comment
Спасибо, это действительно выглядит близко. Но как это может быть ошибкой в ​​моем коде, например. мой поток слишком долго держит Canvas заблокированным? (1) Я изменил свой код, чтобы он соответствовал примеру Lunar Lander, но он все равно не работает. (2) Он работает на тысячах устройств, но не на этих нескольких (может быть, десятках). (3) Как уже отмечалось, когда я задерживаю запуск потока, проблем больше нет, кроме как при завершении работы. Эти три пункта могут говорить о глюках этих устройств, верно? Но о проблемах с этими устройствами я ничего не нашел, да и наверняка об этом слышали. - person caw; 17.12.2015
comment
С другой стороны, когда я уменьшаю свой метод doDraw() до рисования только черного фона, проблемы исчезают. Так что это действительно может быть метод doDraw(), занимающий слишком много времени, а затем слишком долго блокирующийся Canvas. Но почему этого не происходит на всех остальных (старых) устройствах? Единственная причина может быть в высоком разрешении? Опять же, я попытался уменьшить его с помощью setFixedSize(). Не работает. Более того, простой Thread.sleep(10); в конце цикла while (mRunning) { }, похоже, тоже решает проблему. Но это хакерство, и я могу проверить это только на одном устройстве, LG G4. - person caw; 17.12.2015
comment
Вы можете добавить несколько вызовов System.nanoTime() в начале и в конце цикла и посмотреть, как долго удерживается блокировка. Возможно, следите за максимальной продолжительностью и регистрируйте ее каждые 3 секунды (затем сбрасывайте максимальное значение), чтобы избежать переполнения журнала. Гораздо лучший подход — добавить пользовательские события systrace с помощью android.os.Trace и зафиксировать трассировку, пока она находится в неудовлетворительном состоянии — см., например. bigflake.com/systrace . Это даст вам хорошее визуальное представление о различных потоках и их взаимодействии. - person fadden; 17.12.2015
comment
Спасибо! Теперь я сохранял System.nanoTime() всегда до вызова lockCanvas() и после вызова unlockCanvasAndPost(c). Дельта регистрировалась в System.out каждые 4 с. Результаты: (1) Приложение всегда зависало в самом начале. (2) Через 4 с (т. е. ведение журнала) приложение начало реагировать и оставалось таковым до конца. (3) Максимальное время блокировки 125 мс было достигнуто в начале и никогда не превышалось. (4) Во время выключения приложение снова зависло. Вывод: предполагаемая взаимоблокировка возникает только один раз в начале и один раз в конце. Каждый раз, когда очищается один раз, он ушел. - person caw; 19.12.2015
comment
Кроме того, никогда не генерируется никаких исключений (во время блокировки/разблокировки). И каждая захваченная блокировка позже снова освобождается. Как ни странно, в обоих интервалах 0s...4s и 4s...8s метод lockCanvas() вызывается примерно 24 раза в секунду. Судя по всему, поток рендеринга всегда работает нормально. И я могу убедиться, что doDraw() в MySurfaceView тоже работает. Так что, вероятно, только Activity зашел в тупик. Но он должен ожидать некоторых объектов или методов из потока рендеринга или SurfaceView. В противном случае Thread.sleep() в потоке рендеринга не поможет. - person caw; 19.12.2015

Столкнулся с такой же проблемой на xiaomi mi 5, android 6. Активность с канвасом зависала при запуске и через некоторое время после выхода из этой активности. Решил эту проблему, используя lockHardwareCanvas() вместо lockCanvas(). Этот метод недоступен в android 6 напрямую, поэтому я вызвал sHolder.getSurface().lockHardwareCanvas(); и sHolder.getSurface().unlockCanvasAndPost(canvas); Теперь никаких задержек не требуется, работает нормально

person Sevastyanov Nikita    schedule 28.03.2021