Ошибки anr unity

Сообщений: 2 • Страница 1 из 1

Выявление ANR ошибок

Сообщение wokernick99 25 окт 2021, 16:56

Здравствуйте. Опубликовал приложение на плей маркете. Выявил некоторое количество ANR ошибок, их процент превышает допустимые 0.47%. Хочу исправить это положение. Понимаю, что anr ошибка возникает, когда приложение не отвечает в течение 5 секунд. Вопрос в следующем: как по anr ошибке выяснить в каком моменте во время использования приложения оно возникает? Собственно, как понять, куда копать, чтобы ее исправить? Спасибо за будущие ответы.

wokernick99
UNец
 
Сообщения: 1
Зарегистрирован: 25 окт 2021, 16:55

Аватара пользователя
DbIMok
Адепт
 
Сообщения: 6343
Зарегистрирован: 31 июл 2009, 14:05


Сообщений: 2 • Страница 1 из 1

Вернуться в Общие вопросы

Кто сейчас на конференции

Сейчас этот форум просматривают: Google [Bot] и гости: 12



Время на прочтение
4 мин

Количество просмотров 24K

ANR (Application Not Responding) — ошибка, которая возникает, когда приложение не отвечает. В итоге открывается диалоговое окно, предлагающее пользователю подождать или закрыть приложение.
image alt

Условия возникновения ANR

  • Входные события (кнопки и сенсорные события) не обрабатываются в течение 5 секунд;
  • BroadcastReceiver (onRecieve()) не был обработан в течение указанного времени (foreground — 10 с, background — 60 с);
  • ContentProvider не завершен в течение 10 секунд.

Обычно основной поток блокируется.

Если вы читали мои статьи, то наверно уже привыкли к тому, что мы лезет в исходный код. Так что давайте посмотрим как выглядит ANR под капотом.

Класс AppErrors занимается обработкой не только ANR, но и других ошибок, которые могут возникнуть в приложении, включая crash. Метод handleShowAnrUi() как раз и открывает это страшное для многих разработчиков и пользователей окно, отображающее ANR.

class AppErrors {
    ...
    
    void handleShowAnrUi(Message msg) {
        Dialog dialogToShow = null;
        synchronized (mService) {
            AppNotRespondingDialog.Data data = (AppNotRespondingDialog.Data) msg.obj;
            final ProcessRecord proc = data.proc;
            if (proc == null) {
                Slog.e(TAG, "handleShowAnrUi: proc is null");
                return;
            }
            if (proc.anrDialog != null) {
                Slog.e(TAG, "App already has anr dialog: " + proc);
                MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_ANR,
                        AppNotRespondingDialog.ALREADY_SHOWING);
                return;
            }

            Intent intent = new Intent("android.intent.action.ANR");
            if (!mService.mProcessesReady) {
                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
                        | Intent.FLAG_RECEIVER_FOREGROUND);
            }
            mService.broadcastIntentLocked(null, null, intent,
                    null, null, 0, null, null, null, AppOpsManager.OP_NONE,
                    null, false, false, MY_PID, Process.SYSTEM_UID, 0 /* TODO: Verify */);

            boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),
                    Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;
            if (mService.canShowErrorDialogs() || showBackground) {
                dialogToShow = new AppNotRespondingDialog(mService, mContext, data);
                proc.anrDialog = dialogToShow;
            } else {
                MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_ANR,
                        AppNotRespondingDialog.CANT_SHOW);
                // Just kill the app if there is no dialog to be shown.
                mService.killAppAtUsersRequest(proc, null);
            }
        }
        // If we've created a crash dialog, show it without the lock held
        if (dialogToShow != null) {
            dialogToShow.show();
        }
    }

...

Однако, ANR начинается не здесь. Как я и говорила выше, одна из первых причин возникновения этой ошибки — это задержка входного события, которая составляет 5 секунд. Недолгим поиском мы можем найти где задается это значение.

namespace android {
// Default input dispatching timeout if there is no focused application or paused window
// from which to determine an appropriate dispatching timeout.
const nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL; // 5 sec

Теперь мы можем посмотреть в коде, где вызывается нативная часть. Это происходит в классе InputManagerService.

    // Native callback.
    private long notifyANR(InputApplicationHandle inputApplicationHandle,
            InputWindowHandle inputWindowHandle, String reason) {
        return mWindowManagerCallbacks.notifyANR(
                inputApplicationHandle, inputWindowHandle, reason);
    }

А вот и mWindowManagerCallbacks в InputMonitor:

        if (appWindowToken != null && appWindowToken.appToken != null) {
            // Notify the activity manager about the timeout and let it decide whether
            // to abort dispatching or keep waiting.
            final AppWindowContainerController controller = appWindowToken.getController();
            final boolean abort = controller != null
                    && controller.keyDispatchingTimedOut(reason,
                            (windowState != null) ? windowState.mSession.mPid : -1);
            if (!abort) {
                // The activity manager declined to abort dispatching.
                // Wait a bit longer and timeout again later.
                return appWindowToken.mInputDispatchingTimeoutNanos;
            }
        } else if (windowState != null) {
            try {
                // Notify the activity manager about the timeout and let it decide whether
                // to abort dispatching or keep waiting.
                long timeout = ActivityManager.getService().inputDispatchingTimedOut(
                        windowState.mSession.mPid, aboveSystem, reason);
                if (timeout >= 0) {
                    // The activity manager declined to abort dispatching.
                    // Wait a bit longer and timeout again later.
                    return timeout * 1000000L; // nanoseconds
                }
            } catch (RemoteException ex) {
            }
        }
        return 0; // abort dispatching
    }

Давайте посмотрим внимательнее на inputDispatchingTimedOut(). Тут как раз мы и показываем сообщение через ActivityManager об истечении времени ожидания и даем пользователю решить, следует ли отменить действие или продолжить ожидание. И именно в ActivityManagerService и вызывается AppErrors в случае возникновения crash или ANR.

  private boolean makeAppCrashingLocked(ProcessRecord app,
            String shortMsg, String longMsg, String stackTrace) {
        app.crashing = true;
        app.crashingReport = generateProcessError(app,
                ActivityManager.ProcessErrorStateInfo.CRASHED, null, shortMsg, longMsg, stackTrace);
        startAppProblemLocked(app);
        app.stopFreezingAllLocked();
        return handleAppCrashLocked(app, shortMsg, longMsg, stackTrace);
    }
    private void makeAppNotRespondingLocked(ProcessRecord app,
            String activity, String shortMsg, String longMsg) {
        app.notResponding = true;
        app.notRespondingReport = generateProcessError(app,
                ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING,
                activity, shortMsg, longMsg, null);
        startAppProblemLocked(app);
        app.stopFreezingAllLocked();
    }

Основные причины ANR

  • Блокировка ввода/вывода
  • Перегрузка сети
  • Блокировка потоков
  • Бесконечный цикл
  • Бизнес-логика выполняется слишком долго

Как избежать ANR

  • Главный поток пользовательского интерфейса выполняет логику, связанную только с пользовательским интерфейсом;
  • Сложные вычисления (например, операции с базой данных, операции ввода-вывода, сетевые операции и т.д.) производятся в отдельном потоке;
  • Используйте Handler для взаимодействия между потоком пользовательского интерфейса и рабочим потоком;
  • Используйте RxJava и т.д. для обработки асинхронных операций.

Как поймать ANR

  • Информация об ANR может храниться в файле /data/anr/traces.txt, либо по другому пути /data/anr/anr_*. Получить ее можно с помощью следующих команд:
    adb root
    adb shell ls /data/anr
    adb pull /data/anr/<filename>
  • Использовать проект с открытым исходным кодом ANR-WatchDog для обнаружения ANR
  • См. Как избежать ANR :)

P.S. Все подборки я публикую в телеграм канале @paradisecurity.

[REQUIRED] Please fill in the following fields:

  • Unity editor version: 2019.4.29F1
  • Firebase Unity SDK version: 8.3.0
  • Source you installed the SDK: UPM with tgz
  • Problematic Firebase Component: Initialization
  • Other Firebase Components in use: Crashlytics, Messaging, Analytics, Dynamic Links
  • Additional SDKs you are using: Facebook, Ironsource, Admob, Vungle, AdColony
  • Platform you are using the Unity editor on: Mac, Windows
  • Platform you are targeting: iOS, Android
  • Scripting Runtime: IL2CPP

[REQUIRED] Please describe the issue here:

We have recently updated Firebase Unity SDK From 8.1.0 to 8.3.0, A new ANR seems to appear on 5% Install on Google Play Store,

image
image

ANR
Bootup
 Input dispatching timed out (com.company.game/com.google.firebase.MessagingUnityPlayerActivity, Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago.  Wait queue length: 18.  Wait queue head age: 53764.5ms.)
Oct 1st, 2021, 11:51:52 UTC

STACKTRACE

ANR:  Input dispatching timed out (com.company.game/com.google.firebase.MessagingUnityPlayerActivity, Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago.  Wait queue length: 18.  Wait queue head age: 53764.5ms.)
        at com.google.firebase.app.internal.cpp.JniResultCallback.onCompletion(JniResultCallback.java:149)
        at com.google.firebase.app.internal.cpp.JniResultCallback$TaskCallback.onFailure(JniResultCallback.java:77)
        at com.google.android.gms.tasks.zzk.run(com.google.android.gms:play-services-tasks@@17.2.1:1)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:224)
        at android.app.ActivityThread.main(ActivityThread.java:7561)
        at java.lang.reflect.Method.invoke(Method.java:-2)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:995)

THREADS

Thread 2 - main
        at com.google.firebase.app.internal.cpp.JniResultCallback.onCompletion(JniResultCallback.java:149)
        at com.google.firebase.app.internal.cpp.JniResultCallback$TaskCallback.onFailure(JniResultCallback.java:77)
        at com.google.android.gms.tasks.zzk.run(com.google.android.gms:play-services-tasks@@17.2.1:1)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:224)
        at android.app.ActivityThread.main(ActivityThread.java:7561)
        at java.lang.reflect.Method.invoke(Method.java:-2)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:995)

Thread 5869 - Jit thread pool worker thread 0
        at unknown method(unknown file)

Thread 5870 - Signal Catcher
        at unknown method(unknown file)

Thread 5871 - HeapTaskDaemon
        at unknown method(unknown file)

Thread 5872 - ReferenceQueueDaemon
        at java.lang.Object.wait(Object.java:-2)
        at java.lang.Object.wait(Object.java:442)
        at java.lang.Object.wait(Object.java:568)
        at java.lang.Daemons$ReferenceQueueDaemon.runInternal(Daemons.java:217)
        at java.lang.Daemons$Daemon.run(Daemons.java:139)
        at java.lang.Thread.run(Thread.java:919)

Thread 5873 - FinalizerDaemon
        at java.lang.Object.wait(Object.java:-2)
        at java.lang.Object.wait(Object.java:442)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:190)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:211)
        at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:273)
        at java.lang.Daemons$Daemon.run(Daemons.java:139)
        at java.lang.Thread.run(Thread.java:919)

Thread 5874 - FinalizerWatchdogDaemon
        at java.lang.Object.wait(Object.java:-2)
        at java.lang.Object.wait(Object.java:442)
        at java.lang.Object.wait(Object.java:568)
        at java.lang.Daemons$FinalizerWatchdogDaemon.sleepUntilNeeded(Daemons.java:341)
        at java.lang.Daemons$FinalizerWatchdogDaemon.runInternal(Daemons.java:321)
        at java.lang.Daemons$Daemon.run(Daemons.java:139)
        at java.lang.Thread.run(Thread.java:919)

Thread 5875 - Binder:2023_1
        at unknown method(unknown file)

Thread 5876 - Binder:2023_2
        at unknown method(unknown file)

Thread 5877 - Binder:2023_3
        at unknown method(unknown file)

Thread 5878 - Profile Saver
        at unknown method(unknown file)

Thread 5884 - Crashlytics Exception Handler1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at com.google.firebase.crashlytics.internal.common.ExecutorUtils$1$1.onRun(ExecutorUtils.java:64)
        at com.google.firebase.crashlytics.internal.common.BackgroundPriorityRunnable.run(BackgroundPriorityRunnable.java:27)
        at java.lang.Thread.run(Thread.java:919)

Thread 5887 - GmsDynamite
        at java.lang.Object.wait(Object.java:-2)
        at java.lang.Object.wait(Object.java:442)
        at java.lang.Object.wait(Object.java:568)
        at cw.run(:com.google.android.gms.dynamite_dynamiteloader@213614081@21.36.14 (120400-0):2)

Thread 5888 - pool-6-thread-1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5889 - awaitEvenIfOnMainThread task continuation executor1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at com.google.firebase.crashlytics.internal.common.ExecutorUtils$1$1.onRun(ExecutorUtils.java:64)
        at com.google.firebase.crashlytics.internal.common.BackgroundPriorityRunnable.run(BackgroundPriorityRunnable.java:27)
        at java.lang.Thread.run(Thread.java:919)

Thread 5890 - com.google.firebase.crashlytics.startup1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at com.google.firebase.crashlytics.internal.common.ExecutorUtils$1$1.onRun(ExecutorUtils.java:64)
        at com.google.firebase.crashlytics.internal.common.BackgroundPriorityRunnable.run(BackgroundPriorityRunnable.java:27)
        at java.lang.Thread.run(Thread.java:919)

Thread 5892 - Firebase-Messaging-Init
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1120)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:849)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@17.6.0:2)
        at java.lang.Thread.run(Thread.java:919)

Thread 5893 - Firebase-Messaging-Topics-Io
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1120)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:849)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@17.6.0:2)
        at java.lang.Thread.run(Thread.java:919)

Thread 5897 - queued-work-looper
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:181)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 5900 - FirebaseInstanceId
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1120)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:849)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@17.6.0:2)
        at java.lang.Thread.run(Thread.java:919)

Thread 5905 - AsyncTask #1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:459)
        at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)
        at java.util.concurrent.SynchronousQueue.take(SynchronousQueue.java:920)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5907 - pool-17-thread-1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:230)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2109)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1132)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:849)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5910 - pool-19-thread-1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1120)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:849)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5916 - pool-21-thread-1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5919 - pool-18-thread-1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1120)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:849)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5920 - pool-21-thread-2
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5922 - UnityMain
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:868)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1023)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1334)
        at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:232)
        at com.google.firebase.crashlytics.internal.common.Utils.awaitEvenIfOnMainThread(Utils.java:112)
        at com.google.firebase.crashlytics.internal.common.CrashlyticsController.handleUncaughtException(CrashlyticsController.java:231)
        at com.google.firebase.crashlytics.internal.common.CrashlyticsController$1.onUncaughtException(CrashlyticsController.java:153)
        at com.google.firebase.crashlytics.internal.common.CrashlyticsUncaughtExceptionHandler.uncaughtException(CrashlyticsUncaughtExceptionHandler.java:54)
        at com.unity3d.player.n.uncaughtException(Unknown:94)
        at com.bugsnag.android.ExceptionHandler.uncaughtException(ExceptionHandler.java:71)
        at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1073)
        at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
        at java.lang.Thread.dispatchUncaughtException(Thread.java:2187)

Thread 5923 - pool-21-thread-3
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5925 - pool-21-thread-4
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5929 - AssetPackBackgroundExecutor
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5930 - AssetPackService
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:181)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 5933 - Binder:2023_4
        at unknown method(unknown file)

Thread 5936 - AudioPortEventHandler
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:181)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 5937 - AudioTrack
        at unknown method(unknown file)

Thread 5941 - bugsnag-anr-collector
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:181)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 5942 - process reaper
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:230)
        at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:461)
        at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)
        at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:937)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1091)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5946 - ConnectivityThread
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:181)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 5953 - Thread-12
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:181)
        at com.ironsource.mediationsdk.sdk.j$a.run(Unknown:10)

Thread 5954 - Timer-0
        at java.lang.Object.wait(Object.java:-2)
        at java.lang.Object.wait(Object.java:442)
        at java.util.TimerThread.mainLoop(Timer.java:559)
        at java.util.TimerThread.run(Timer.java:512)

Thread 5955 - Timer-1
        at java.lang.Object.wait(Object.java:-2)
        at java.lang.Object.wait(Object.java:442)
        at java.util.TimerThread.mainLoop(Timer.java:559)
        at java.util.TimerThread.run(Timer.java:512)

Thread 5956 - UnityChoreographer
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:181)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 5960 - logThread
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:181)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 5961 - httpThread
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:181)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 5967 - pool-27-thread-1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5968 - pool-28-thread-1
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Thread 5969 - Thread-18
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1120)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:849)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at com.appsflyer.AFExecutor$1$4.run(unknown:31)
        at java.lang.Thread.run(Thread.java:919)

Thread 5971 - Thread-20
        at com.google.firebase.app.internal.cpp.JniResultCallback.nativeOnResult(JniResultCallback.java:-2)
        at com.google.firebase.app.internal.cpp.JniResultCallback.onCompletion(JniResultCallback.java:151)
        at com.google.firebase.app.internal.cpp.JniResultCallback.cancel(JniResultCallback.java:143)

Thread 5972 - GoogleApiHandler
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:181)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 5975 - Thread-22
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1120)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:849)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at com.appsflyer.AFExecutor$1$4.run(unknown:31)
        at java.lang.Thread.run(Thread.java:919)

Thread 5976 - MessengerIpcClient
        at sun.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1120)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:849)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@17.6.0:2)
        at java.lang.Thread.run(Thread.java:919)

Thread 5986 - Thread-31
        at com.google.firebase.app.internal.cpp.JniResultCallback.onCompletion(JniResultCallback.java:149)
        at com.google.firebase.app.internal.cpp.JniResultCallback$TaskCallback.onFailure(JniResultCallback.java:77)
        at com.google.android.gms.tasks.zzk.run(com.google.android.gms:play-services-tasks@@17.2.1:1)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:224)
        at android.app.ActivityThread.main(ActivityThread.java:7561)
        at java.lang.reflect.Method.invoke(Method.java:-2)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:995)

Google Play Store Screenshot:
image

Search Issue Tracker

Fixed in 2022.1.X

Found in

2018.4.28f1

2019.4

2020.3

2021.1

2021.2

2022.1

Reproduction steps:

1. Open the attached user’s project «Unity request 1112926 demo.zip»

2. Switch to Android and Build to a device

3. Tap the button on the screen

4. Then immediately start dragging your finger on the screen in circles for several seconds

Expected result: the APP doesn’t crash

Actual result: the APP becomes unresponsive and crashes with ANR errors

Reproduces on: 2019.4.31f1, 2020.3.21f1, 2021.1.27f1, 2021.2.0b16, 2022.1.0a11

Reproducible with these devices:

N/A, Samsung Galaxy S9 (SM-G960F), Android 8.0.0, CPU: Exynos 9 Series 9810, GPU: Mali-G72

VLNQA00318, Oneplus 7Pro (GM1913), Android 10, CPU: Snapdragon 855 SM8150, GPU: Adreno (TM) 640

VLNQA00231, Huawei HUAWEI Mate 20 Pro (LYA-L29), Android 9, CPU: HiSilicon Kirin 980, GPU: Mali-G76

Приложение отвечает: как мы уменьшили количество ANR-ошибок в шесть раз. Часть 1, про сбор данных +12

Разработка под Android, Тестирование мобильных приложений, Аналитика мобильных приложений, Блог компании Badoo, Google API


Рекомендация: подборка платных и бесплатных курсов 3D-моделирования — https://katalog-kursov.ru/

Пожалуй, одна из худших проблем, которая может случиться с вашим приложением, — ошибка ANR (Application Not Responding), когда приложение не отвечает. Если таких ошибок много, они могут негативно влиять не только на пользовательский опыт, но и на позицию в выдаче Google Play и фичеринг. 

В начале прошлого года количество ANRs в приложении Badoo превышало порог “Bad Behaviour” в Google Play. Поэтому мы собрали команду для решения этой проблемы и потратили несколько месяцев, экспериментируя с разными подходами. В результате мы смогли уменьшить количество таких ошибок более чем в шесть раз.

В этой серии из двух статей я расскажу о том, как нам это удалось, что дало наибольший эффект и как вы можете использовать эти подходы в своём приложении.

В первой части мы поговорим об основах: что представляет собой ошибка ANR и как её лучше отслеживать. Если вы уже знакомы с этой темой, предлагаю перейти ко второй части, в которой я расскажу о наших способах решения этой проблемы.

Что такое ошибка ANR?

Обычно любое приложение с графическим интерфейсом выполняет все связанные с ним операции и отрисовку в отдельном UI-потоке исполнения. Android не исключение: здесь в главном потоке приложения выполняется цикл, отвечающий за все действия с интерфейсом:

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

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

Когда UI-поток Android-приложения блокируется слишком долго, выдаётся ошибка Application Not Responding (ANR).

ANR выдаётся, когда приложение находится в одном из этих состояний:

— на переднем плане находится Activity, приложение в течение пяти секунд не отвечает на входящие события или BroadcastReceiver, например нажатия на кнопки или касания экрана;

— на переднем плане нет Activity, ваш BroadcastReceiver не закончил исполнение в течение длительного времени.

Если ANR случается, когда на переднем плане находится Activity вашего приложения, Android показывает диалоговое окно с предложением закрыть приложение или подождать.

Довольно легко принудительно вызвать ANR, написав Thread.sleep() в любом обработчике интерфейса, например обработчик нажатия кнопки. После нажатия на кнопку вы увидите примерно следующее:

Наличие ошибок ANR в вашем приложении не только влияет на опыт его использования, но и, согласно документации Google, может повлиять на позицию в поисковой выдаче и продвижение в Google Play.

Чтобы снизить вероятность возникновения ANR, нужно всего лишь избегать выполнения длительных операций в главном потоке. Звучит вроде бы просто, но иногда не так легко определить корневую проблему, которая приводит к таким ошибкам. Поэтому довольно важно иметь хорошую систему мониторинга и репортинга ANR-ошибок.

Давайте посмотрим, какие существуют способы отладки ANR-ошибок и какие инструменты могут быть в этом полезны.

Отслеживание ANR

Локальный анализ

Самый простой случай — если у вас есть возможность стабильно воспроизводить ANR-проблему локально. Существует довольно много инструментов, которые могут помочь вам быстро найти источник проблемы.

Первое, что можно сделать, — это проверить дамп стек-трейсов для всех потоков (thread dump). Когда приложение перестает отвечать, Android создаёт дамп всех текущих потоков, который может помочь в анализе проблемы. Обычно он находится в директории /data/anr/, точный путь можно найти в Logcat сразу после сообщения об ошибке ANR.

Дамп потоков содержит стек-трейсы: вы увидите, в каком состоянии был каждый поток (например, какая строка выполнялась в конкретный момент времени). По сути, это состояние приложения на момент создания дампа.

Чаще всего причина возникновения ANR обнаруживается в стек-трейсе главного потока скорее всего, код в этом месте выполняется слишком долго. Если информации из этого стек-трейса будет недостаточно, можно попробовать обратиться к довольно неплохой документации от Google, где описываются основные причины, способы диагностирования и решения проблемы ANR.

Отслеживание с помощью Google Play

Google Play автоматически отправляет отчёты об ошибках ANR, если у пользователя включена такая опция. В консоли Google Play есть несколько метрик и инструментов для анализа ANR.

Во-первых, можно увидеть агрегированные графики с общим количеством ANR-ошибок за день. Также есть такая метрика, как ANR rate — отношение количества сессий за день, в которых возникала хотя бы одна ANR-ошибка, к общему количеству сессий за сутки. Для этой метрики задан порог в 0,47%, превышение которого считается «неудовлетворительным поведением» (“Bad Behaviour”) и может плохо повлиять на позицию приложения в Google Play. 

Во-вторых, можно открывать отдельные отчёты об ANR-ошибках, сгруппированные по схожести на основе стек-трейса. Основные группы находятся в разделе Android Vitals. И это, вероятно, наиболее полезный раздел для выявления самых частых причин возникновения ANR-ошибок в вашем приложении.

Если вы активно используете консоль Google Play, вы могли заметить некоторые её недостатки. Например, к отчётам нельзя прикрепить дополнительную информацию, такую как логи для отладки. Также невозможно настроить логику группировки отчётов. Иногда система помещает в одну группу ошибки, возникшие по разным причинам, а иногда раскидывает по разным группам ошибки, у которых причина одна.

Всё это иногда затрудняет определение основных ошибок и поиск изначальных проблем. Что же можно сделать для улучшения ситуации?

Скачивание данных из Google Play

Для решения проблемы с логикой группировки можно попробовать скачать сырые отчёты об ANR-ошибках из Google Play для последующего ручного анализа. Раньше была возможность выгрузить эти данные из Google Cloud Storage, но несколько лет назад Google перестала поддерживать этот функционал:

Однако всё ещё можно просматривать отдельные отчёты в консоли. Но как нам экспортировать тысячи отчётов, не потратив при этом кучу времени на рутинную работу?

Существует много способов автоматизировать сбор информации с сайтов. Самый правильный и простой — получение данных через API, но, к сожалению, Google не предоставляет публичный API для получения отчётов. Одно из решений — эмулировать пользовательское поведение, автоматически кликая на ссылки и кнопки в браузере и сохраняя отображающийся текст.

Веб-скрапер можно реализовать с помощью популярного инструмента Selenium, который предоставляет простой интерфейс для взаимодействия с веб-страницами. Изначально он предназначался для создания автоматизированных тестов для веб-приложений и доступен на разных языках, включая Java и Kotlin.

Мы реализовали скрапер на Selenium и получили сырые отчёты об ANR-ошибках для одного из релизов. Благодаря этому нам удалось проанализировать их так, как не получилось бы сделать с помощью встроенных в консоль Google Play инструментов. Например, просто поискав в отчётах по ключевым словам “Application.onCreate”, мы обнаружили, что около 60% ошибок произошло во время выполнения метода Application.onCreate. При этом в консоли Google Play нет возможности получить такую информацию, так как отчёты разбиты по группам.

Внутренняя аналитика

Другой способ сбора дополнительных данных и проведения расширенного анализа заключается в настройке собственного репортинга ANR-ошибок. В прошлом мы уже экспериментировали с решением похожих проблем, настраивая репортинг крашей. Для того чтобы проводить анализ падений приложения, мы создали внутренний инструмент Gelato.

Его функциональность схожа с возможностями других инструментов для краш-репортинга, таких как Firebase Crashlytics и App Center, но ещё и позволяет нам полностью контролировать сохраняемые данные, менять логику группировки и применять сложную фильтрацию:

Это не реальные данные приложения Bumble, иллюстрация сделана просто для примера

Это не реальные данные приложения Bumble, иллюстрация сделана просто для примера

Мы решили отслеживать в Gelato ещё и ANR-ошибки в надежде, что это поможет нам в поиске их причин. Для этого нам нужно было знать, когда приложение перестаёт отвечать. В Android 11 появился новый API, предоставляющий информацию о недавних причинах завершения процесса, но у большинства наших пользователей установлены более ранние версии ОС, поэтому нам требовалось найти другое решение.

И мы нашли простой способ, который часто используется для отслеживания зависаний главного потока исполнения: запустить watchdog-поток, который периодически будет пытаться выполнить задачу в главном потоке. Если задача не выполняется за определённый промежуток времени, то можно сохранить дамп текущего состояния потоков и отправить его в наш инструмент для анализа отчётов о падениях:

Такую логику реализует, например, библиотека, которой мы воспользовались для реализации репортинга в Gelato. Это позволило нам проводить более глубокий анализ данных и лучше интегрировать этот инструмент в нашу инфраструктуру. Например, теперь мы можем сравнивать зависания главного потока в разных вариантах в ходе A/B-тестирования.

Вот пример отчёта в нашей системе:

Это не реальные данные приложения Bumble, иллюстрация сделана просто для примера

Это не реальные данные приложения Bumble, иллюстрация сделана просто для примера

Полезный совет: собирайте и отправляйте вместе с отчётом лог событий аналитики. Иногда это даёт возможность буквально пошагово воспроизвести проблему. 

Если у вас нет своего решения для сбора отчётов о падениях приложения, вы можете настроить репортинг и в сторонние инструменты. Например, можно отправлять ANR-ошибки в App Center или Firebase Crashlytics, так как они предоставляют API для отправки кастомных крашей.

Но помните, что все эти отчёты нельзя считать полной альтернативой ANR-отчётам в Google Play (как мы говорили выше, в Android немного другие правила определения таких ошибок). Но в любом случае это может помочь получить общее представление об основных проблемах. Вполне вероятно, что если генерируется много отчётов о зависании главного потока исполнения в какой-то части вашего приложения, то в ней происходят и ANR-ошибки. 

В завершение

Мы обсудили, что представляют собой ANR-ошибки и как их можно отслеживать. Во второй части статьи я расскажу о наших подходах к снижению ANR rate и о том, что из этого получилось.

Ссылки

  1. Android Vitals в Google Play

  2. Отладка ANR

  3. API для получения причин завершения процесса

  4. Фреймворк для тестирования веб-страниц

  5. Библиотека для определения зависаний

Понравилась статья? Поделить с друзьями:
  • Ошибки airmatic w220
  • Ошибки airbag mazda 6 gh
  • Ошибки activex com что это
  • Ошибки activex com speechruntime exe toastnotifier
  • Ошибки activex com inprocserver32