Ошибки ANR (просмотры)

Основные понятия и реализация Jetpack Compose

Если поток UI приложения Android заблокирован слишком долго, возникает ошибка "Приложение не отвечает" (ANR). Если приложение работает в активном режиме, система показывает пользователю диалоговое окно, как на рисунке 1. В диалоговом окне ANR пользователю предлагается принудительно закрыть приложение.

Пользователь видит диалоговое окно с сообщением об ошибке ANR.
Рисунок 1. Пользователь видит диалоговое окно с сообщением об ошибке ANR.

Ошибки ANR возникают, когда основной поток приложения, отвечающий за обновление интерфейса, не может обработать действия пользователя или отрисовать экран. Это мешает работе с приложением. Подробнее об основном потоке приложения…

Ошибка ANR возникает в следующих случаях:

  • Превышено время ожидания ввода. Приложение не реагирует на ввод (например, нажатие клавиши или касание экрана) в течение пяти секунд.
  • Выполнение сервиса. Если сервис, объявленный в приложении, не может завершить выполнение методов Service.onCreate() и Service.onStartCommand()/Service.onBind() в течение нескольких секунд.
  • **Service.startForeground() не вызывается**. Если приложение использует Context.startForegroundService() для запуска новой активной службы, но она не вызывает startForeground() в течение пяти секунд.
  • Трансляция намерения. Если выполнение BroadcastReceiver не завершилось в течение заданного времени. Если в приложении есть действия на переднем плане, время ожидания составляет 10 секунд.
  • **JobScheduler взаимодействия**. Если JobService не возвращается из JobService.onStartJob() или JobService.onStopJob() в течение нескольких секунд или если запускается задача, инициированная пользователем, и ваше приложение не вызывает JobService.setNotification() в течение нескольких секунд после вызова JobService.onStartJob(). В приложениях для Android 13 и более ранних версий ошибки ANR не регистрируются и не передаются в приложение. В приложениях для Android 14 и более поздних версий ошибки ANR регистрируются и передаются в приложение.

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

Устраните проблемы

Определив проблему, вы можете воспользоваться советами из этого раздела, чтобы устранить распространенные неполадки.

Медленный код в основном потоке

Определите, в каких местах кода основной поток приложения занят более пяти секунд. Попробуйте найти подозрительные сценарии использования в приложении и воспроизвести ошибку ANR.

Например, на рисунке 2 показана временная шкала Traceview, на которой основной поток занят более пяти секунд.

Рис. 2. Временная шкала Traceview, на которой показан занятый основной поток

Рисунок 2. Временная шкала Traceview, на которой показан занятый основной поток

На рисунке 2 показано, что большая часть проблемного кода находится в обработчике onClick(View), как показано в следующем примере кода:

Kotlin

override fun onClick(v: View) {
    // This task runs on the main thread.
    BubbleSort.sort(data)
}

Java

@Override
public void onClick(View view) {
    // This task runs on the main thread.
    BubbleSort.sort(data);
}

В этом случае вам следует перенести работу, которая выполняется в основном потоке, в рабочий поток. В Android Framework есть классы, которые помогают перенести задачу в рабочий поток. Подробнее о рабочих потоках…

Операция ввода-вывода в основном потоке

Выполнение операций ввода-вывода в основном потоке – распространенная причина медленной работы основного потока, которая может приводить к ошибкам ANR. В Compose разработчики часто случайно запускают чтение с диска (например, SharedPreferences или вызовы базы данных), пытаясь получить начальное состояние.

Выполняйте длительные операции ввода-вывода отдельно от уровня пользовательского интерфейса. Используйте withContext(Dispatchers.IO) в ViewModel или, что ещё лучше, используйте репозиторий на уровне данных. Рекомендуется перенести все операции ввода-вывода в рабочий поток, как показано в предыдущем разделе.

Примеры операций ввода-вывода: операции с сетью и хранилищем. Подробную информацию можно найти в статьях Выполнение сетевых операций и Сохранение данных.

Конфликт блокировок

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

Например, на рисунке 3 показана временная шкала Traceview, на которой большая часть работы выполняется в рабочем потоке.

Рис. 3. Временная шкала Traceview, на которой показана работа, выполняемая в рабочем потоке

Рисунок 3. Временная шкала Traceview, на которой показана работа, выполняемая в рабочем потоке

Если же ошибки ANR по-прежнему возникают, проверьте статус основного потока в Android Device Monitor. Обычно основной поток находится в состоянии RUNNABLE, если он готов обновить интерфейс и в целом реагирует на действия пользователя.

Если же основной поток не может возобновить выполнение, он находится в состоянии BLOCKED и не может реагировать на события. В Android Device Monitor статус будет показан как Monitor или Wait (см. рис. 5).

Рис. 4. Основной поток в статусе Monitor

Рисунок 4. Основной поток в статусе "Мониторинг"

На трассировке ниже показан основной поток приложения, заблокированный в ожидании ресурса.

...
AsyncTask #2" prio=5 tid=18 Runnable
  | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
  | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
  | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
  | stack=0x94a7e000-0x94a80000 stackSize=1038KB
  | held mutexes= "mutator lock"(shared held)
  at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
  - locked <0x083105ee> (a java.lang.Boolean)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
  at android.os.AsyncTask$2.call(AsyncTask.java:305)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
  at java.lang.Thread.run(Thread.java:761)
...

Проанализировав трассировку, вы сможете найти код, который блокирует основной поток. Следующий код отвечает за удержание блокировки, которая блокирует основной поток в предыдущей трассировке:

Kotlin

override fun onClick(v: View) {
    // The worker thread holds a lock on lockedResource
    LockTask().execute(data)

    synchronized(lockedResource) {
        // The main thread requires lockedResource here
        // but it has to wait until LockTask finishes using it.
    }
}

class LockTask : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? =
            synchronized(lockedResource) {
                // This is a long-running operation, which makes
                // the lock last for a long time
                BubbleSort.sort(params[0])
            }
}

Java

@Override
public void onClick(View v) {
    // The worker thread holds a lock on lockedResource
  new LockTask().execute(data);

  synchronized (lockedResource) {
      // The main thread requires lockedResource here
      // but it has to wait until LockTask finishes using it.
  }
}

public class LockTask extends AsyncTask<Integer[], Integer, Long> {
  @Override
  protected Long doInBackground(Integer[]... params) {
      synchronized (lockedResource) {
          // This is a long-running operation, which makes
          // the lock last for a long time
          BubbleSort.sort(params[0]);
      }
  }
}

Другой пример – основной поток приложения, ожидающий результата от рабочего потока, как показано в следующем коде. Обратите внимание, что использование wait() и notify() не рекомендуется в Kotlin, поскольку в этом языке есть собственные механизмы для обработки параллелизма. При использовании Kotlin по возможности применяйте механизмы, характерные для этого языка.

Kotlin

fun onClick(v: View) {
    val lock = java.lang.Object()
    val waitTask = WaitTask(lock)
    synchronized(lock) {
        try {
            waitTask.execute(data)
            // Wait for this worker thread's notification
            lock.wait()
        } catch (e: InterruptedException) {
        }
    }
}

internal class WaitTask(private val lock: java.lang.Object) : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        synchronized(lock) {
            BubbleSort.sort(params[0])
            // Finished, notify the main thread
            lock.notify()
        }
    }
}

Java

public void onClick(View v) {
  WaitTask waitTask = new WaitTask();
  synchronized (waitTask) {
      try {
          waitTask.execute(data);
          // Wait for this worker thread’s notification
          waitTask.wait();
      } catch (InterruptedException e) {}
  }
}

class WaitTask extends AsyncTask<Integer[], Integer, Long> {
  @Override
  protected Long doInBackground(Integer[]... params) {
      synchronized (this) {
          BubbleSort.sort(params[0]);
          // Finished, notify the main thread
          notify();
      }
  }
}

Основной поток может быть заблокирован и в других ситуациях, например при использовании Lock, Semaphore, пула ресурсов (например, пула подключений к базе данных) или других механизмов взаимного исключения (блокировки).

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

Убедитесь, что блокировки удерживаются как можно меньше времени, или даже оцените, нужны ли они вообще. Если вы используете блокировку, чтобы определить, когда нужно обновить интерфейс на основе обработки рабочего потока, используйте такие механизмы, как onProgressUpdate() и onPostExecute(), для связи между рабочим и основным потоками.

Медленные широковещательные приемники

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

Ошибка ANR возникает в следующих случаях:

  • Широковещательный приемник не завершил выполнение метода onReceive в течение значительного времени.
  • Широковещательный приемник вызывает goAsync, но не вызывает finish для объекта PendingResult.

В методе onReceive класса BroadcastReceiver должны выполняться только короткие операции. Однако если вашему приложению требуется более сложная обработка в результате оповещения, вам следует отложить задачу до ViewModel (используя возможности сопрограмм, областей видимости и диспетчеров Kotlin), если ожидается, что задача займет не более нескольких секунд, любого типа хранилища состояния или до WorkManager для задач, которые, как ожидается, займут больше нескольких секунд.

Чтобы определить, выполняет ли широковещательный приемник продолжительные операции в основном потоке приложения, можно использовать такие инструменты, как Traceview. Например, на рисунке 6 показана временная шкала широковещательного приемника, который обрабатывает сообщение в основном потоке примерно 100 секунд.

Рис. 5. Хронология Traceview, показывающая работу `BroadcastReceiver` в основном потоке

Рисунок 5. Хронология Traceview, показывающая работу BroadcastReceiver в основном потоке

Это может быть вызвано выполнением продолжительных операций в методе onReceive() класса BroadcastReceiver, как показано в следующем примере:

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    // This is a long-running operation
    BubbleSort.sort(data)
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // This is a long-running operation
    BubbleSort.sort(data);
}

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

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    Intent(context, MyIntentService::class.java).also { intentService ->
        // The task now runs on a worker thread.
        context.startService(intentService)
    }
}

class MyIntentService : IntentService("MyIntentService") {
    override fun onHandleIntent(intent: Intent?) {
        BubbleSort.sort(data)
    }
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // The task now runs on a worker thread.
    Intent intentService = new Intent(context, MyIntentService.class);
    context.startService(intentService);
}

public class MyIntentService extends IntentService {
  @Override
  protected void onHandleIntent(@Nullable Intent intent) {
      BubbleSort.sort(data);
  }
}

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

Рис. 6. Временная шкала Traceview, на которой показано, что оповещение обрабатывается в рабочем потоке

Рисунок 6. Временная шкала Traceview, на которой показано, что оповещение обрабатывается в рабочем потоке

Широковещательный приемник может использовать goAsync(), чтобы сообщить системе, что ему нужно больше времени на обработку сообщения. Однако вам нужно вызвать finish() для объекта PendingResult. В следующем примере показано, как вызвать finish(), чтобы система могла повторно использовать широковещательный приемник и избежать ошибки ANR:

Kotlin

val pendingResult = goAsync()

object : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        // This is a long-running operation
        BubbleSort.sort(params[0])
        pendingResult.finish()
        return 0L
    }
}.execute(data)

Java

final PendingResult pendingResult = goAsync();
new AsyncTask<Integer[], Integer, Long>() {
  @Override
  protected Long doInBackground(Integer[]... params) {
      // This is a long-running operation
      BubbleSort.sort(params[0]);
      pendingResult.finish();
  }
}.execute(data);

Однако перенос кода из медленного широковещательного приемника в другой поток и использование goAsync() не устранят ошибку ANR, если трансляция выполняется в фоновом режиме. Время ожидания для ошибки ANR остается прежним.