Freeze (hang) is a state where a mobile application stops responding to any user actions for an extended period. Unlike lags (slowdown) and glitches (incorrect behavior), a freeze completely blocks the UI: touches are not processed, animation stops, the screen “freezes”. The cause is blocking the main thread with a synchronous operation, a deadlock in multithreaded code, or abnormally long garbage collection. According to Apple Main Thread Checker Documentation, more than 40% of iOS crash reports are related to main thread blocking. On Android, a similar situation leads to ANR — the system dialog “App not responding”.
Key Takeaways
Freeze (hang) in a mobile application is a state where the app stops processing input events and updating the interface for several seconds or more. Technically, this means the main thread is blocked and cannot execute the next run loop iteration.
A lag is a delay of up to 500 ms where the user notices slowdown but the application continues to work. Freeze lasts from 1 second to tens of seconds. ANR on Android is a special case of a freeze that lasted more than 5 seconds and was detected by the system. Not every freeze leads to ANR, but every ANR is a system-documented freeze.
On Android, a freeze lasting more than 5 seconds triggers an ANR dialog offering to close the app. On iOS, the system has a watchdog — if the app does not respond to events for 10–20 seconds, the Watchdog terminates the process with code 0x8badf00d (ate bad food). The user only sees the app suddenly closing to the home screen.
Any operation that takes longer than 100 ms and is launched on the main thread can potentially cause a freeze. Let’s look at the main sources of blockages.
Reading a large file, a network request without asynchrony, saving data to SharedPreferences using the synchronous apply method followed by commit — all these operations block the main thread. On Android, synchronously reading a 10 MB file can take 200–500 ms depending on flash memory speed. On iOS, a synchronous URLSession load without a completionHandler blocks the UI for the server response time.
When two threads wait for resources held by each other, a deadlock occurs. In mobile applications, a typical scenario is thread A locks Lock1 and waits for Lock2, while thread B locks Lock2 and waits for Lock1. Both threads freeze forever. If one of them is the main thread, the application freezes completely.
A logic error — for example, while(true) without an exit condition or recursion without a base case — leads to infinite execution on the main thread. Android detects this via ANR after 5 seconds, iOS — via Stackshot, which captures an infinitely repeating call stack.
Diagnosing freezes requires tools capable of capturing the state of all threads at the moment of blockage.
On each ANR, the Android system saves a file /data/anr/traces.txt containing a stack dump of each application thread. Analyzing this file is the main diagnostic method: find the main thread and see which method it stopped on. If the stack ends with Thread.sleep, InputStream.read, or Lock.lock — the cause is found.
Xcode can take a Stackshot — a snapshot of all thread stacks — when the application freezes (SIGSTOP signal). Enable “Logging” → “Include Stackshot Logs” in the scheme. On a crash with code 0x8badf00d, extract the crash log from Devices & Simulators and find the com.apple.main-thread with the stuck stack.
Main Thread Checker automatically detects UIKit calls from background threads while the app is running. Enable it in the scheme (Diagnostics → Main Thread Checker). Each warning is a potential cause of a freeze, especially if it occurs in a network request completionHandler closure.
Example of detecting blockages via StrictMode on Android:
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())
}
}
Eliminating freezes starts with moving all potentially long operations to background threads. Let’s look at specific techniques for each platform.
Kotlin Coroutines with viewModelScope.launch(Dispatchers.IO) ensure that network operations or database reads execute on a background thread. Dispatchers.Main is used only for UI updates. Important: all suspend functions must be structured — child coroutines are cancelled when the parent is cancelled, preventing thread leaks.
Grand Central Dispatch with DispatchQueue.global(qos: .userInitiated) for background tasks and DispatchQueue.main.async for UI updates is the standard pattern. Avoid sync() on the main queue — this is a guaranteed deadlock. Use async/await (Swift 5.5+) for more readable asynchronous code with automatic return to the main thread via MainActor.
synchronized blocks in Kotlin and @synchronized in Swift on the main thread are dangerous: if another thread has already acquired this lock, the main thread will freeze waiting. Use atomic types (AtomicInteger, atomic properties in Swift) or serial queues instead of locks.
Example of asynchronous data loading with coroutines on Android:
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)
}
}
}
A combination of tools, architectural principles, and code review processes helps systematically prevent freezes.
Configure StrictMode with penaltyDeath for thread policies — this will cause an immediate app crash when a network call or disk I/O is detected on the main thread. The developer cannot ignore the problem. In production builds, use penaltyLog to collect statistics without crashes.
On iOS, enable Main Thread Checker in the Debug scheme and configure CI to run tests with this option. If a test contains a UIKit call from a background thread — it should fail. This is the only reliable way to identify the problem before sending to TestFlight.
Add a mandatory item to the code review process: check that any network call, file operation, database access, or heavy computation runs on a background thread. Deadlock can be detected with static analyzers: Infer by Facebook and Thread Safety Checker by Xcode find potential locks before runtime.
Frequently Asked Questions
ANR (Application Not Responding) is an Android system notification that appears when the main thread freezes for more than 5 seconds. Freeze is a broader concept: any UI blockage of any duration. iOS does not have ANR, but it has a Watchdog with a 10–20 second timeout.
The file is located at /data/anr/traces.txt. Access requires root access or adb shell: run adb shell cat /data/anr/traces.txt \> traces.txt with root privileges. In the stack, find the “main” thread — the last called method indicates the cause of the blockage.
If the freeze lasts less than 10 seconds, the Watchdog does not trigger, and the app simply “hangs” until the blocking operation completes. The user does not see a crash but experiences frustration. To detect such cases, use MetricKit with custom execution time traces.
Use UI tests with a check that the screen opens in < 1 second. Add time measurement between tap and the next screen appearance to CI. On Android, use Espresso with IdlingResource to wait for asynchronous operations. On iOS, use XCTest with XCTWaiter to check loading time.
SwiftUI itself does not cause freezes, but complex computations in the body property do. If body takes 500 ms to compute due to heavy operations, the UI freezes. The solution is to offload computations to Task.detached and update @State asynchronously on the main actor.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also