Błędy ANR (wyświetlenia)

Pojęcia i implementacja Jetpack Compose

Gdy wątek UI aplikacji na Androida jest zablokowany zbyt długo, pojawia się błąd „Aplikacja nie odpowiada” (ANR). Jeśli aplikacja działa na pierwszym planie, system wyświetla użytkownikowi okno, jak na rysunku 1. Okno ANR umożliwia użytkownikowi wymuszenie zamknięcia aplikacji.

Użytkownikowi wyświetliło się okno z błędem ANR.
Rysunek 1. Wyświetlenie użytkownikowi okna z błędem ANR

Błędy ANR są problemem, ponieważ główny wątek aplikacji, który odpowiada za aktualizowanie interfejsu, nie może przetwarzać zdarzeń wejściowych użytkownika ani rysować, co powoduje frustrację użytkownika. Więcej informacji o głównym wątku aplikacji znajdziesz w omówieniu procesów i wątków.

Błąd ANR jest wywoływany w aplikacji, gdy wystąpi jeden z tych warunków:

  • Przekroczenie limitu czasu wysyłania danych wejściowych: jeśli aplikacja nie odpowie na zdarzenie wejściowe (np. naciśnięcie klawisza lub dotknięcie ekranu) w ciągu 5 sekund.
  • Wykonywanie usługi: jeśli usługa zadeklarowana przez aplikację nie może zakończyć wykonywania metod Service.onCreate() i Service.onStartCommand()/Service.onBind() w ciągu kilku sekund.
  • **Service.startForeground() nie wywołano**: jeśli aplikacja używa Context.startForegroundService() do uruchomienia nowej usługi na pierwszym planie, ale usługa nie wywołuje startForeground() w ciągu 5 sekund.
  • Rozgłaszanie intencji: jeśli BroadcastReceiver nie zakończy się w ustawionym czasie. Jeśli aplikacja jest aktywna na pierwszym planie, limit czasu wynosi 10 sekund.
  • **Interakcje z **JobScheduler**: jeśli JobService nie powróci z JobService.onStartJob() lub JobService.onStopJob() w ciągu kilku sekund albo jeśli rozpocznie się zadanie zainicjowane przez użytkownika, a aplikacja nie wywoła JobService.setNotification() w ciągu kilku sekund od wywołania JobService.onStartJob(). W przypadku aplikacji kierowanych na Androida 13 i starsze wersje błędy ANR są ciche i nie są zgłaszane aplikacji. W przypadku aplikacji kierowanych na Androida 14 i nowsze wersje błędy ANR są jawne i są zgłaszane aplikacji.

Jeśli w aplikacji występują błędy ANR, możesz skorzystać z informacji w tym artykule, aby zdiagnozować i rozwiązać problem.

Rozwiązywanie problemów

Po zidentyfikowaniu problemu możesz skorzystać ze wskazówek w tej sekcji, aby rozwiązać najczęstsze problemy.

Powolny kod w głównym wątku

Zidentyfikuj miejsca w kodzie, w których główny wątek aplikacji jest zajęty przez ponad 5 sekund. Poszukaj w aplikacji podejrzanych przypadków użycia i spróbuj odtworzyć błąd ANR.

Na przykład na rysunku 2 widać oś czasu Traceview, na której wątek główny jest zajęty przez ponad 5 sekund.

Rysunek 2. Oś czasu Traceview pokazująca zajęty wątek główny

Rysunek 2. Oś czasu Traceview pokazująca zajęty wątek główny

Rysunek 2 pokazuje, że większość nieprawidłowego kodu występuje w procedurze obsługi onClick(View), jak pokazano w tym przykładzie kodu:

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

W takim przypadku należy przenieść zadania wykonywane w wątku głównym do wątku instancji roboczej. Platforma Androida zawiera klasy, które mogą pomóc w przeniesieniu zadania do wątku instancji roboczej. Więcej informacji znajdziesz w sekcji Wątki instancji roboczej.

Wejście-wyjście w wątku głównym

Wykonywanie operacji wejścia-wyjścia w wątku głównym jest częstą przyczyną powolnego działania wątku głównego, co może powodować błędy ANR. W Compose deweloperzy często przypadkowo wywołują odczyty dysku (np. SharedPreferences lub wywołania bazy danych) podczas próby uzyskania stanu początkowego.

Długo trwające operacje wejścia-wyjścia wykonuj poza warstwą interfejsu. Używaj withContext(Dispatchers.IO) w ViewModel lub, jeszcze lepiej, używaj repozytorium w warstwie danych. Zalecamy przeniesienie wszystkich operacji wejścia/wyjścia do wątku instancji roboczej, jak pokazano w poprzedniej sekcji.

Przykłady operacji wejścia-wyjścia to operacje sieciowe i operacje na pamięci masowej. Więcej informacji znajdziesz w artykułach Wykonywanie operacji sieciowych i Zapisywanie danych.

Rywalizacja o blokadę

W niektórych przypadkach zadanie, które powoduje błąd ANR, nie jest wykonywane bezpośrednio w głównym wątku aplikacji. Jeśli wątek instancji roboczej blokuje zasób, którego wątek główny potrzebuje do wykonania zadania, może wystąpić błąd ANR.

Na przykład na rysunku 3 widać oś czasu Traceview, na której większość pracy jest wykonywana w wątku instancji roboczej.

Rysunek 3. Oś czasu Traceview, która pokazuje pracę wykonywaną w wątku instancji roboczej

Rysunek 3. Oś czasu Traceview, która pokazuje pracę wykonywaną w wątku instancji roboczej

Jeśli jednak użytkownicy nadal napotykają błędy ANR, sprawdź stan głównego wątku w Android Device Monitor. Zwykle wątek główny jest w stanie RUNNABLE, jeśli jest gotowy do aktualizacji interfejsu i ogólnie reaguje na działania użytkownika.

Jeśli jednak wątek główny nie może wznowić wykonywania, jest w stanie BLOCKED i nie może odpowiadać na zdarzenia. Stan jest wyświetlany w Android Device Monitor jako Monitor (Monitoruj) lub Wait (Czekaj), jak pokazano na rysunku 5.

Rysunek 4. Wątek główny w stanie monitorowania

Rysunek 4. Wątek główny w stanie monitorowania

Poniższy log czasu pokazuje, że wątek główny aplikacji jest zablokowany i czeka na zasób:

...
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)
...

Przejrzenie śladu może pomóc w zlokalizowaniu kodu, który blokuje wątek główny. Ten kod odpowiada za utrzymanie blokady, która blokuje wątek główny w poprzednim logu czasu:

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]);
      }
  }
}

Innym przykładem jest wątek główny aplikacji, który czeka na wynik z wątku instancji roboczej, jak pokazano w poniższym kodzie. Pamiętaj, że używanie wait() i notify() nie jest zalecanym wzorcem w języku Kotlin, który ma własne mechanizmy obsługi równoczesności. Jeśli używasz Kotlina, w miarę możliwości korzystaj z mechanizmów specyficznych dla tego języka.

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();
      }
  }
}

Istnieją inne sytuacje, które mogą blokować wątek główny, w tym wątki korzystające z Lock, Semaphore, a także puli zasobów (takiej jak pula połączeń z bazą danych) lub innych mechanizmów wzajemnego wykluczania (mutex).

Ogólnie rzecz biorąc, należy ocenić blokady zasobów, które są utrzymywane przez aplikację, ale jeśli chcesz uniknąć błędów ANR, sprawdź blokady zasobów wymaganych przez wątek główny.

Upewnij się, że blokady są utrzymywane przez jak najkrótszy czas, a najlepiej sprawdź, czy aplikacja w ogóle ich potrzebuje. Jeśli używasz blokady, aby określić, kiedy zaktualizować interfejs na podstawie przetwarzania wątku instancji roboczej, użyj mechanizmów takich jak onProgressUpdate() i onPostExecute() do komunikacji między instancją roboczą a głównymi wątkami.

Powolne odbiorniki

Aplikacje mogą reagować na komunikaty, takie jak włączenie lub wyłączenie trybu samolotowego albo zmiana stanu połączenia, za pomocą odbiorników komunikatów. Błąd ANR występuje, gdy aplikacja zbyt długo przetwarza komunikat.

Błąd ANR występuje w tych przypadkach:

  • Odbiornik komunikatów nie zakończył wykonywania metody onReceive w odpowiednim czasie.
  • Odbiornik wywołuje metodę goAsync, ale nie wywołuje metody finish obiektu PendingResult.

Aplikacja powinna wykonywać tylko krótkie operacje w metodzie onReceive klasy BroadcastReceiver. Jeśli jednak aplikacja wymaga bardziej złożonego przetwarzania w wyniku otrzymania komunikatu, należy odroczyć zadanie do ViewModel (wykorzystując możliwości współprogramów, zakresów i dyspozytorów w Kotlin), jeśli zadanie ma potrwać co najwyżej kilka sekund, dowolnego typu zmiennej stanu lub do WorkManager w przypadku zadań, które mają potrwać dłużej niż kilka sekund.

Możesz użyć narzędzi takich jak Traceview, aby sprawdzić, czy odbiornik komunikatów wykonuje długo trwające operacje w głównym wątku aplikacji. Na przykład rysunek 6 przedstawia oś czasu odbiorcy komunikatów, który przetwarza wiadomość w głównym wątku przez około 100 sekund.

Rysunek 5. Oś czasu Traceview pokazująca pracę `BroadcastReceiver` w głównym wątku

Rysunek 5. Oś czasu Traceview pokazująca pracę BroadcastReceiver w wątku głównym

Może to być spowodowane wykonywaniem długo trwających operacji w metodzie onReceive() interfejsu BroadcastReceiver, jak w tym przykładzie:

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

W takich sytuacjach zalecamy przeniesienie długo trwającej operacji do IntentService, ponieważ do wykonywania pracy używa on wątku instancji roboczej. Poniższy kod pokazuje, jak używać IntentService do przetwarzania długo trwającej operacji:

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

Dzięki użyciu IntentService długo trwająca operacja jest wykonywana w wątku instancji roboczej, a nie w wątku głównym. Rysunek 7 przedstawia pracę odroczoną do wątku instancji roboczej na osi czasu Traceview.

Rysunek 6. Oś czasu Traceview pokazująca komunikat przetworzony w wątku instancji roboczej

Rysunek 6. Oś czasu Traceview pokazująca komunikat przetworzony w wątku instancji roboczej

Odbiornik może użyć metody goAsync(), aby zasygnalizować systemowi, że potrzebuje więcej czasu na przetworzenie wiadomości. Należy jednak wywołać metodę finish() na obiekcie PendingResult. Poniższy przykład pokazuje, jak wywołać metodę finish(), aby umożliwić systemowi ponowne wykorzystanie odbiornika i uniknąć błędu 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);

Jednak przeniesienie kodu z wolnego odbiorcy komunikatów do innego wątku i użycie goAsync() nie rozwiąże problemu z błędem ANR, jeśli komunikat jest w tle. Limit czasu błędu ANR nadal obowiązuje.