Zawieszanie (wisi) — to stan, w którym aplikacja mobilna przestaje reagować na jakiekolwiek działania użytkownika na dłuższy czas. W przeciwieństwie do lagów (spowolnienia działania) i glitchy (nieprawidłowe zachowanie), zawieszanie całkowicie blokuje UI: dotknięcia nie są przetwarzane, animacja się zatrzymuje, ekran „zastyga”. Przyczyną jest blokada głównego wątku przez operację synchroniczną, deadlock w kodzie wielowątkowym lub anomalnie długie zbieranie śmieci. Według danych Apple Main Thread Checker Documentation, ponad 40% raportów crashy na iOS jest związanych z blokadą głównego wątku. Na Androidzie analogiczna sytuacja prowadzi do ANR — systemowego okna „Aplikacja nie odpowiada".
Najważniejsze
Zawieszanie (freeze, hang) w aplikacji mobilnej — to stan, w którym aplikacja przestaje przetwarzać zdarzenia wejściowe i aktualizować interfejs przez kilka sekund lub dłużej. Technicznie oznacza to, że główny wątek (main thread) jest zablokowany i nie może wykonać kolejnego cyklu runnera.
Lag — to opóźnienie do 500 ms, przy którym użytkownik zauważa spowolnienie, ale aplikacja nadal działa. Zawieszanie trwa od 1 sekundy do dziesiątek sekund. ANR na Androidzie — to szczególny przypadek zawieszania, które trwało ponad 5 sekund i zostało wykryte przez system. Nie każde zawieszanie prowadzi do ANR, ale każde ANR — to udokumentowane przez system zawieszanie.
Na Androidzie zawieszanie dłuższe niż 5 sekund wywołuje okno ANR z propozycją zamknięcia aplikacji. Na iOS system ma watchdog — jeśli aplikacja nie reaguje na zdarzenia przez 10–20 sekund, Watchdog kończy proces z kodem 0x8badf00d (ate bad food). Użytkownik widzi jedynie nagłe zamknięcie aplikacji i powrót do ekranu głównego.
Każda operacja, która wykonuje się dłużej niż 100 ms i jest uruchomiona w głównym wątku, potencjalnie powoduje zawieszanie. Rozważmy główne źródła blokad.
Odczyt dużego pliku, zapytanie sieciowe bez asynchroniczności, zapisywanie danych w SharedPreferences synchroniczną metodą apply z następującym commit — wszystkie te operacje blokują główny wątek. Na Androidzie synchroniczny odczyt pliku o rozmiarze 10 MB może zająć 200–500 ms w zależności od szybkości pamięci flash. Na iOS synchroniczne ładowanie URLSession bez completionHandler blokuje UI na czas odpowiedzi serwera.
Gdy dwa wątki oczekują na zwolnienie zasobów trzymanych przez siebie nawzajem, powstaje deadlock. W aplikacjach mobilnych typowy scenariusz — wątek A blokuje Lock1 i czeka na Lock2, a wątek B blokuje Lock2 i czeka na Lock1. Oba wątki zawieszają się na zawsze. Jeśli jeden z nich to główny wątek, aplikacja zawiesza się całkowicie.
Błąd w logice — na przykład while(true) bez warunku wyjścia lub rekurencja bez przypadku bazowego — prowadzi do nieskończonego wykonywania na głównym wątku. Android wykrywa to poprzez ANR po 5 sekundach, iOS — przez Stackshot, który rejestruje nieskończenie powtarzający się stos wywołań.
Diagnostyka zawieszania wymaga narzędzi zdolnych do zarejestrowania stanu wszystkich wątków w momencie blokady.
Przy każdym ANR system Android zapisuje plik /data/anr/traces.txt, który zawiera zrzut stosu każdego wątku aplikacji. Analiza tego pliku — główna metoda diagnostyki: należy znaleźć wątek main i sprawdzić, na której metodzie się zatrzymał. Jeśli stos kończy się na Thread.sleep, InputStream.read lub Lock.lock — przyczyna znaleziona.
Xcode przy zawieszaniu aplikacji (sygnał SIGSTOP) może wykonać Stackshot — zrzut stosów wszystkich wątków. Włącz w schemacie „Logging" → „Include Stackshot Logs". Przy awarii z kodem 0x8badf00d wyodrębnij log crash z Devices & Simulators i znajdź wątek com.apple.main-thread z zawieszonym stosem.
Main Thread Checker automatycznie wykrywa wywołania UIKit z wątków tła podczas działania aplikacji. Włącz go w schemacie (Diagnostics → Main Thread Checker). Każde ostrzeżenie — potencjalna przyczyna zawieszania, szczególnie jeśli występuje w zamknięciu completionHandler zapytania sieciowego.
Przykład wykrywania blokady przez StrictMode na Androidzie:
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build())
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build())
}
}
Usuwanie zawieszania zaczyna się od przeniesienia wszystkich potencjalnie długich operacji do wątków tła. Rozważmy konkretne techniki dla każdej platformy.
Kotlin Coroutines z viewModelScope.launch(Dispatchers.IO) gwarantują, że operacja sieciowa lub odczyt z bazy danych są wykonywane w wątku tła. Dispatchers.Main jest używany tylko do aktualizacji UI. Ważne: wszystkie suspend-funkcje powinny być strukturalne — korutyny potomne są anulowane przy anulowaniu rodzica, zapobiegając wyciekowi wątków.
Grand Central Dispatch z DispatchQueue.global(qos: .userInitiated) dla zadań w tle i DispatchQueue.main.async do aktualizacji UI — standardowy wzorzec. Unikaj sync() na głównej kolejce — to gwarantowany deadlock. Używaj async/await (Swift 5.5+) dla bardziej czytelnego kodu asynchronicznego z automatycznym powrotem do głównego wątku przez MainActor.
Blokady synchronized w Kotlin i @synchronized w Swift na głównym wątku są niebezpieczne: jeśli inny wątek już przejął tę blokadę, główny wątek zawiesi się w oczekiwaniu. Używaj atomowych typów (AtomicInteger, atomowe właściwości w Swift) lub kolejki sekwencyjne zamiast blokad.
Przykład asynchronicznego ładowania danych z korutynami na Androidzie:
class DataViewModel : ViewModel() {
private val _data = MutableStateFlow<List<Item>>(emptyList())
val data: StateFlow<List<Item>> = _data.asStateFlow()
fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
val result = fetchFromNetwork()
_data.emit(result)
}
}
}
Zapobieganie zawieszaniu systemowo pomaga kombinacja narzędzi, zasad architektonicznych i procesów code review.
Skonfiguruj StrictMode z penaltyDeath dla polityk wątkowych — spowoduje to natychmiastową awarię aplikacji przy wykryciu wywołania sieciowego lub wejścia-wyjścia dysku na głównym wątku. Programista nie będzie mógł zignorować problemu. W kompilacji produkcyjnej używaj penaltyLog do zbierania statystyk bez awarii.
Na iOS włącz Main Thread Checker w schemacie Debug i skonfiguruj CI do uruchamiania testów z tą opcją. Jeśli test zawiera wywołanie UIKit z wątku tła — powinien zakończyć się niepowodzeniem. To jedyny niezawodny sposób na wykrycie problemu przed wysłaniem do TestFlight.
Dodaj do procesu code review obowiązkowy punkt: sprawdzenie, że każde wywołanie sieciowe, praca z plikami, bazą danych lub ciężkie obliczenia są wykonywane w wątku tła. Deadlock można wykryć analizatorem statycznym: Infer od Facebook i Thread Safety Checker od Xcode znajdują potencjalne blokady przed uruchomieniem.
Często zadawane pytania
ANR (Application Not Responding) — to systemowe powiadomienie Androida, które pojawia się przy zawieszaniu głównego wątku na ponad 5 sekund. Zawieszanie to szersze pojęcie: każda blokada UI dowolnego czasu. Na iOS nie ma ANR, ale jest Watchdog z limitem 10–20 sekund.
Plik znajduje się w /data/anr/traces.txt. Do dostępu wymagany jest root lub adb shell: wykonaj adb shell cat /data/anr/traces.txt \> traces.txt z prawami root. W stosie znajdź wątek „main" — ostatnia wywołana metoda wskazuje na przyczynę blokady.
Jeśli zawieszanie trwa krócej niż 10 sekund, Watchdog nie uruchamia się, a aplikacja po prostu „wisi" do zakończenia operacji blokującej. Użytkownik nie widzi awarii, ale doświadcza frustracji. Do wykrywania takich przypadków używaj MetricKit z niestandardowymi śladami czasu wykonania.
Używaj testów UI ze sprawdzeniem, czy ekran otwiera się w mniej niż 1 sekundę. Dodaj do CI pomiar czasu między dotknięciem a pojawieniem się następnego ekranu. Na Androidzie używaj Espresso z IdlingResource do oczekiwania na operacje asynchroniczne. Na iOS XCTest z XCTWaiter do sprawdzenia czasu ładowania.
SwiftUI sam w sobie nie powoduje zawieszania, ale złożone obliczenia we właściwości body — tak. Jeśli body oblicza się przez 500 ms z powodu ciężkich operacji, UI się zawiesza. Rozwiązanie — przenieś obliczenia do Task.detached i aktualizuj @State asynchronicznie na głównym aktorze.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również