Zawieszanie się w programowaniu — istota, przyczyny i zapobieganie

Autor: IT Sectr Opublikowano: 2026-07-28 Czas czytania: 9 min

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 — całkowita blokada UI na dłuższy czas (sekundy i dziesiątki sekund), różniąca się od lagów i glitchy
  • Główne przyczyny — blokada głównego wątku operacją wejścia-wyjścia, deadlock między wątkami, nieskończona pętla i wyciek pamięci z długim GC
  • Diagnostyka obejmuje Main Thread Checker na iOS, logi ANR /data/anr/traces.txt na Androidzie i analizę zrzutów wątków
  • Usuwanie — przeniesienie wszystkich potencjalnie długich operacji do wątków tła, użycie Structured Concurrency i unikanie synchronized w wątku UI
  • Profilaktyka — StrictMode, Main Thread Checker w schemacie Debug, analiza statyczna pod kątem deadlock i okresowe uruchamianie testów z pomiarem czasu odpowiedzi

Czym jest zawieszanie w programowaniu mobilnym

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.

Różnica między zawieszaniem a lagiem i ANR

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.

Konsekwencje zawieszania

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.

Przyczyny zawieszania na Androidzie i iOS

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.

Synchroniczne wejście-wyjście w wątku UI

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.

Deadlock w kodzie wielowątkowym

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.

Nieskończona pętla lub rekurencja

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

  • Android — Cursor bez zamknięcia, synchroniczne zapytanie przez execute() zamiast enqueue(), FileInputStream.read() w wątku UI
  • iOS — performSelectorOnMainThread:withObject:waitUntilDone:YES, uruchomienie NSURLConnection sendSynchronousRequest, ładowanie obrazu przez dataWithContentsOfURL
  • Cross-platform — Flutter compute bez wydzielonego isolate, React Native synchroniczny NativeModule

Jak diagnozować zawieszanie

Diagnostyka zawieszania wymaga narzędzi zdolnych do zarejestrowania stanu wszystkich wątków w momencie blokady.

Logi ANR na Androidzie

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.

Stackshot na iOS

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 w Xcode

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:

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

Metody usuwania blokad UI

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.

Structured Concurrency z korutynami

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.

Asynchroniczne kolejki na iOS

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.

Unikanie synchronized w wątku UI

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:

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

Profilaktyka zawieszania na etapie programowania

Zapobieganie zawieszaniu systemowo pomaga kombinacja narzędzi, zasad architektonicznych i procesów code review.

StrictMode z penaltyDeath

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.

Main Thread Checker w schemacie Debug

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.

Peer Review ze sprawdzeniem wielowątkowości

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.

  • Android — StrictMode, Infer, Android Lint Multithread, Kotlin Coroutines z viewModelScope
  • iOS — Main Thread Checker, TSAN (Thread Sanitizer), Xcode Analyze, Swift async/await z MainActor
  • Cross-platform — Flutter compute isolate, React Native interaction manager z requestAnimationFrame

Często zadawane pytania

Jaka jest różnica między zawieszaniem a ANR?

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.

Jak odczytać traces.txt na Androidzie?

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.

Dlaczego aplikacja zawiesza się na iOS, ale nie ulega awarii?

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.

Jak testować aplikację pod kątem zawieszania?

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.

Czy SwiftUI może powodować zawieszanie?

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

  • Zawieszanie — całkowita blokada UI na sekundy i dziesiątki sekund, spowodowana blokadą głównego wątku, deadlock lub nieskończoną pętlą
  • Diagnostyka — /data/anr/traces.txt na Androidzie, Stackshot i Main Thread Checker na iOS
  • Główne przyczyny — synchroniczne wejście-wyjście, deadlock między wątkami, nieskończona rekurencja, długi GC
  • Usuwanie — korutyny z odpowiednimi dyspaczerami, async/await z MainActor, przeniesienie wszystkich operacji IO do wątków tła
  • Profilaktyka — StrictMode z penaltyDeath, Main Thread Checker, analiza statyczna deadlock (Infer, TSAN)
  • Na Androidzie zawieszanie > 5 s = ANR; na iOS > 10–20 s = Watchdog crash (0x8badf00d)
  • Zalecenie: włącz Thread Sanitizer w schemacie Debug i skonfiguruj CI do uruchamiania testów z TSAN w celu wykrywania data race i deadlock

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.

Omów projekt

Przeczytaj również