Skip to content

Anr and performance

ANR and Performance Deep Dive

Overview

ANR (Application Not Responding) occurs when the main thread blocks too long. The OS detects unresponsiveness and kills the app. Understanding main thread work, threading models, and performance profiling is crucial for shipping responsive Android apps.


ANR Fundamentals

What is ANR?

ANR = Main Thread blocks > Timeout threshold

OS detects main thread blocked โ†’ Kills app โ†’ "App not responding" dialog

Timeout Thresholds

Component Timeout
Foreground Activity 5 seconds
Foreground Service 5 seconds
Background Service 60 seconds
Broadcast Receiver 10 seconds
Content Provider 10 seconds

When ANR Triggered

Example: 5-second block in foreground Activity

t=0s: User taps button
      onClick() starts heavy operation

t=5s: Main thread still blocked
      OS detects no response

t=5s: ANR kill triggered
      App process terminated

      System shows:
      "AppName has stopped responding"
      User can close or wait

Consequences

  • App crashes
  • Bad review
  • Users uninstall
  • Play Store penalties
  • Lost time investment

Main Thread Responsibilities

What Main Thread Does

  1. Handle User Input
  2. Tap, scroll, swipe
  3. Must respond immediately (<100ms perceived)

  4. Update UI

  5. Draw to screen
  6. Update views
  7. Animations

  8. Render Frames

  9. 60fps = 16.67ms per frame
  10. 90fps = 11.11ms per frame
  11. 120fps = 8.33ms frametime

  12. Process System Messages

  13. Lifecycle callbacks
  14. System broadcasts
  15. Messages from Handler

Why NOT to Block Main Thread

If main thread blocked for I/O:

t=0s: Network request starts on main thread
t=0s-5s: BLOCKED
       - Input events queued (UI feels frozen)
       - Frames skipped (animation stutters)
       - User can't scroll

t=5s: ANR kill

Blocking Operations (What NOT to do)

Network I/O

// โŒ WRONG - ANR risk
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // This blocks main thread!
    val response = URL("https://api.example.com/data").readText()
    updateUI(response)
}

File I/O

// โŒ WRONG - ANR risk
val file = File("/path/to/large/file")
val data = file.readBytes()  // Blocks! Could be seconds

Database Queries

// โŒ WRONG on main thread
val users = database.userDao().getAllUsers()  // Room blocks main thread
displayUsers(users)

Heavy Computation

// โŒ WRONG
val result = expensiveAlgorithm(largeDataset)  // Can take seconds

Image Processing

// โŒ WRONG
val bitmap = BitmapFactory.decodeFile(largImagePath)  // Decoding slow
imageView.setImageBitmap(bitmap)

Solutions: Threading Models

Solution 1: Plain Thread

// โœ… CORRECT - off main thread
Thread {
    val response = URL("https://api.example.com/data").readText()

    // Post back to main thread for UI
    runOnUiThread {
        updateUI(response)
    }
}.start()

Downside: Manual management, verbose, error-prone

Solution 2: AsyncTask (Legacy)

// โœ… CORRECT but deprecated
class FetchDataTask : AsyncTask<Void, Void, String>() {
    override fun doInBackground(vararg params: Void?): String {
        // Background thread
        return URL("https://api.example.com/data").readText()
    }

    override fun onPostExecute(result: String) {
        // Main thread
        updateUI(result)
    }
}

FetchDataTask().execute()

Solution 3: Coroutines (BEST - MODERN)

// โœ… CORRECT - recommended
viewModelScope.launch {
    try {
        // Switched to IO dispatcher automatically
        val data = withContext(Dispatchers.IO) {
            URL("https://api.example.com/data").readText()
        }

        // Back on main dispatcher
        updateUI(data)
    } catch (e: Exception) {
        showError(e)
    }
}

Why coroutines best: - Clean, readable syntax - Automatic cancellation with scope - Exception handling - Less boilerplate

Solution 4: LiveData + Repository

// โœ… CORRECT Architecture
class MyViewModel : ViewModel() {
    val data = dataRepository.observeData()  // LiveData
}

class DataRepository {
    fun observeData(): LiveData<Data> =
        liveData(Dispatchers.IO) {
            emit(fetchFromNetwork())  // Off main thread
        }
}

// Activity
viewModel.data.observe(this) { data ->
    updateUI(data)  // Main thread
}

Jank and Frame Timing

What is Jank?

Jank = Visible stutter in UI, animation, scroll

Root cause: Dropping frames due to main thread blocking

Frame Timing Budget

60fps device:
  Frame time budget = 16.67ms

If main thread work > 16.67ms:
  Frame skipped
  User sees stutter
  = JANK

Multiple frames > budget:
  Multiple stutters
  Looks very janky

Example: Janky Scroll

Frame 1: 10ms (โœ“ on time)
Frame 2: 5ms (โœ“ on time)
Frame 3: 25ms (โŒ late - frame dropped, stutter visible)
Frame 4: 8ms (โœ“ on time)
Frame 5: 30ms (โŒ late - stutter again)

Result: Janky scrolling experience

Causes of Jank

  1. Heavy computation on main thread
  2. Inefficient view hierarchy
  3. Too many views to measure/layout
  4. Nested layouts
  5. Overdraw
  6. Drawing same pixels multiple times
  7. Memory pressure
  8. GC pauses
  9. Excessive allocations
  10. I/O on main thread
  11. Reading files
  12. Database queries
  13. Bitmap scaling/processing
  14. Not in background
  15. Layout thrashing
  16. Measuring in draw pass
  17. Large view animations
  18. Complex transformations

Measuring Performance

1. Profile GPU Rendering (Device Settings)

Settings โ†’ Developer options โ†’ Profile GPU rendering โ†’ On screen as bars

Each bar represents one frame:
  Green = Good (under 16.67ms)
  Yellow = Warning (approaching deadline)
  Red = Bad (over deadline, frame dropped)

2. Android Studio Profiler

1. Run app
2. Android Studio โ†’ Profiler
3. Record CPU/Memory/etc
4. View frame times graph
5. Identify slow frames

3. Layout Inspector

1. Run app
2. Tools โ†’ Layout Inspector
3. See view hierarchy
4. Identify excessive nesting

4. Systrace

# Record trace
python systrace.py gfxinfo view -o trace.html

# Open HTML file in browser
# View frame rendering timeline

5. FrameMetrics API

// Programmatically measure frame times
onFrameMetricsAvailable { metrics ->
    val frameDuration = metrics.getMetric(FrameMetricsAggregator.TOTAL_INDEX)
    Log.d("Frame", "Duration: $frameDuration ms")
}

Optimization Strategies

1. Reduce View Hierarchy

// โŒ Many nested layouts
<LinearLayout>
    <LinearLayout>
        <LinearLayout>
            <View/>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

// โœ… Flat hierarchy
<FrameLayout>
    <View/>
</FrameLayout>

2. Use Efficient Collections

// In RecyclerView
val notifyList = mutableListOf<Int>()  // Always notifyDataSetChanged

// โœ… Better: notify specific items
notifyItemRangeChanged(0, 5)

3. Lazy Load Data

// โœ… Show loading placeholder first
// Load full data in background
// Update when ready

4. Defer Non-Critical Work

// โœ… Defer analytics
analyticsQueue.add(event)
// Send later, not immediately

5. Use Smaller Images

// โŒ Load full size (2000x2000)
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.large)

// โœ… Load optimized size
val options = BitmapFactory.Options().apply {
    inSampleSize = 4  // 1/4 size
}
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.large, options)

Common ANR Scenarios

Scenario 1: Network Call on Main

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // โŒ Can block 5+ seconds
    val response = httpClient.newCall(request).execute().body()?.string()
}

Fix: Use coroutines

viewModelScope.launch {
    val response = withContext(Dispatchers.IO) {
        httpClient.newCall(request).execute().body()?.string()
    }
    updateUI(response)
}

Scenario 2: Database Query on Main

// โŒ Room blocks on main thread by default on older versions
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val users = database.userDao().loadAllUsers()  // Blocks!
}

Fix: Use LiveData

class UserViewModel : ViewModel() {
    val users = database.userDao().getAllUsersLive()  // Returns LiveData
}

// In Activity
viewModel.users.observe(this) { users ->
    updateUI(users)
}

Scenario 3: ContentResolver Query

// โŒ CAN block many seconds
override fun onCreate(savedInstanceState: Bundle?) {
    val cursor = contentResolver.query(
        ContactsContract.Contacts.CONTENT_URI,
        null, null, null, null
    )  // Might have thousands of contacts!
}

Fix: Query on background thread

viewModelScope.launch(Dispatchers.IO) {
    val contacts = loadContactsFromProvider()

    withContext(Dispatchers.Main) {
        displayContacts(contacts)
    }
}


Key Takeaways

โœ… ANR = Main thread blocked > timeout

โœ… Keep main thread operations < 100-200ms

โœ… Never do network, I/O, heavy computation on main thread

โœ… Use coroutines (best), AsyncTask (old), or plain threads

โœ… Measure with Profiler or GPS rendering

โœ… Reduce view hierarchy depth

โœ… Lazy load non-critical data

โœ… Profile before optimizing