Сообщений: 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) — ошибка, которая возникает, когда приложение не отвечает. В итоге открывается диалоговое окно, предлагающее пользователю подождать или закрыть приложение.
Условия возникновения 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,
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:
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, но ещё и позволяет нам полностью контролировать сохраняемые данные, менять логику группировки и применять сложную фильтрацию:
Мы решили отслеживать в Gelato ещё и ANR-ошибки в надежде, что это поможет нам в поиске их причин. Для этого нам нужно было знать, когда приложение перестаёт отвечать. В Android 11 появился новый API, предоставляющий информацию о недавних причинах завершения процесса, но у большинства наших пользователей установлены более ранние версии ОС, поэтому нам требовалось найти другое решение.
И мы нашли простой способ, который часто используется для отслеживания зависаний главного потока исполнения: запустить watchdog-поток, который периодически будет пытаться выполнить задачу в главном потоке. Если задача не выполняется за определённый промежуток времени, то можно сохранить дамп текущего состояния потоков и отправить его в наш инструмент для анализа отчётов о падениях:
Такую логику реализует, например, библиотека, которой мы воспользовались для реализации репортинга в Gelato. Это позволило нам проводить более глубокий анализ данных и лучше интегрировать этот инструмент в нашу инфраструктуру. Например, теперь мы можем сравнивать зависания главного потока в разных вариантах в ходе A/B-тестирования.
Вот пример отчёта в нашей системе:
Полезный совет: собирайте и отправляйте вместе с отчётом лог событий аналитики. Иногда это даёт возможность буквально пошагово воспроизвести проблему.
Если у вас нет своего решения для сбора отчётов о падениях приложения, вы можете настроить репортинг и в сторонние инструменты. Например, можно отправлять ANR-ошибки в App Center или Firebase Crashlytics, так как они предоставляют API для отправки кастомных крашей.
Но помните, что все эти отчёты нельзя считать полной альтернативой ANR-отчётам в Google Play (как мы говорили выше, в Android немного другие правила определения таких ошибок). Но в любом случае это может помочь получить общее представление об основных проблемах. Вполне вероятно, что если генерируется много отчётов о зависании главного потока исполнения в какой-то части вашего приложения, то в ней происходят и ANR-ошибки.
В завершение
Мы обсудили, что представляют собой ANR-ошибки и как их можно отслеживать. Во второй части статьи я расскажу о наших подходах к снижению ANR rate и о том, что из этого получилось.
Ссылки
-
Android Vitals в Google Play
-
Отладка ANR
-
API для получения причин завершения процесса
-
Фреймворк для тестирования веб-страниц
-
Библиотека для определения зависаний