Błędy ANR (wyświetlenia)

Koncepcje i implementacja w Jetpack Compose

Gdy wątek UI aplikacji na Androida jest zablokowany przez zbyt długi czas, wywoływany jest błąd „Aplikacja nie odpowiada” (ANR). Jeśli aplikacja jest na pierwszym planie, system wyświetla użytkownikowi okno, jak pokazano na ilustracji 1. Okno ANR daje użytkownikowi możliwość wymuszenia zamknięcia aplikacji.

Okno ANR wyświetlane użytkownikowi.
Ilustracja 1. Okno ANR wyświetlane użytkownikowi

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 artykule Omówienie 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 odpowiedziała 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 Service.onCreate() i Service.onStartCommand()/Service.onBind() w ciągu kilku sekund.
  • **Service.startForeground() nie jest wywoływana:** 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ł wykonywania w określonym czasie. Jeśli aplikacja ma aktywność na pierwszym planie, ten limit czasu wynosi 5 sekund.
  • **JobScheduler interakcje**: jeśli JobService nie zwróci wartoś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 po wywołaniu 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 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ęściej spotykane problemy.

Powolny kod w wątku głównym

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

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

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

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

Ilustracja 2 pokazuje, że większość problematycznego kodu znajduje się w obsłudze 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ść pracę wykonywaną w wątku głównym do wątku roboczego. Android Framework zawiera klasy, które mogą pomóc w przeniesieniu zadania do wątku instancji roboczej. Więcej informacji znajdziesz w artykule Wątki robocze.

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

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

Długotrwałe operacje wejścia-wyjścia wykonuj poza warstwą interfejsu. Użyj withContext(Dispatchers.IO) w ViewModel lub, co jeszcze lepsze, użyj repozytorium w warstwie danych. Zalecamy przeniesienie wszystkich operacji wejścia-wyjścia do wątku roboczego, jak pokazano w poprzedniej sekcji.

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

Rywalizacja o blokadę

W niektórych przypadkach praca powodująca błąd ANR nie jest wykonywana bezpośrednio w głównym wątku aplikacji. Jeśli wątek roboczy ma blokadę zasobu, którego główny wątek potrzebuje do wykonania swojej pracy, może wystąpić błąd ANR.

Na przykład na ilustracji 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 roboczym

Ilustracja 3. Oś czasu Traceview pokazująca pracę wykonywaną w wątku roboczym

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

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

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

Ilustracja 4. Główny wątek w stanie Monitor

Ten ślad pokazuje główny wątek aplikacji, który 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)
...

Przeanalizowanie śladu może pomóc w znalezieniu kodu, który blokuje główny wątek. Ten kod odpowiada za blokowanie głównego wątku w poprzednim śladzie:

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 główny wątek aplikacji, który czeka na wynik z wątku instancji roboczej, jak pokazano w tym kodzie. Pamiętaj, że używanie wait() i notify() nie jest zalecanym wzorcem w Kotlinie, który ma własne mechanizmy obsługi współbieżności. Jeśli używasz Kotlina, w miarę możliwości stosuj mechanizmy specyficzne 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ą też inne sytuacje, które mogą blokować główny wątek, w tym wątki, które używają Lock, Semaphore, a także puli zasobów (np. puli połączeń z bazą danych) lub innych mechanizmów wzajemnego wykluczania (mutex) .

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

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

Powolne odbiorniki

Aplikacje mogą odpowiadać na komunikaty rozgłaszane, np. włączanie lub wyłączanie trybu samolotowego albo zmiana stanu połączenia, za pomocą odbiorników. Błąd ANR występuje, gdy przetwarzanie komunikatu rozgłaszanego przez aplikację trwa zbyt długo.

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

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

Za pomocą narzędzi takich jak Traceview możesz sprawdzić, czy odbiornik wykonuje długotrwałe operacje w głównym wątku aplikacji. Na przykład na ilustracji 6 widać oś czasu odbiornika, który przetwarza komunikat w wątku głównym przez około 100 sekund.

Rysunek 5. Oś czasu Traceview pokazująca działanie elementu `BroadcastReceiver` w głównym wątku

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

Takie zachowanie może być spowodowane wykonywaniem długotrwałych operacji w metodzie onReceive() odbiornika BroadcastReceiver, jak pokazano 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 an IntentService ponieważ używa ona wątku instancji roboczej do wykonywania swojej pracy. Ten 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. Ilustracja 7 pokazuje pracę odłożoną do wątku instancji roboczej na osi czasu Traceview.

Rysunek 6. Oś czasu Traceview pokazująca komunikat rozgłoszeniowy przetwarzany w wątku instancji roboczej

Ilustracja 6. Oś czasu Traceview pokazująca komunikat rozgłaszany przetwarzany w wątku instancji roboczej

Odbiornik może używać goAsync(), aby zasygnalizować systemowi, że potrzebuje więcej czasu na przetworzenie komunikatu. Należy jednak wywołać finish() w obiekcie PendingResult. Ten przykład pokazuje, jak wywołać finish(), aby umożliwić systemowi ponowne użycie 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);

Jeśli jednak przeniesiesz kod z powolnego odbiornika do innego wątku i użyjesz goAsync(), nie rozwiążesz problemu z błędem ANR, jeśli rozgłaszanie odbywa się w tle. Limit czasu ANR nadal obowiązuje.