Detecting ANRs in Your Android App: A Developer‘s Guide

Cover image of Android robot with magnifying glass

If you‘ve spent much time developing Android apps, you‘re probably all too familiar with the dreaded "Application Not Responding" dialog. ANRs are one of the most disruptive and frustrating issues that can plague an app, leading to poor reviews, reduced engagement, and even app abandonment.

As a professional Android developer, it‘s crucial to have a solid understanding of what causes ANRs and a robust toolset for detecting and troubleshooting them. In this in-depth guide, we‘ll cover everything you need to know to keep your app responsive and ANR-free.

ANR 101: Causes and Thresholds

Let‘s start with the basics: what exactly is an ANR? An "Application Not Responding" error occurs when an app‘s main thread (aka the UI thread) is blocked for too long and unable to process user input events or draw.

Specifically, an ANR is triggered when the main thread fails to respond to an input event within 5 seconds, or a BroadcastReceiver hasn‘t finished executing within 10 seconds. Prior to Android 11, the thresholds were even tighter at 5 seconds for input events and broadcasts.

Some common scenarios that frequently cause ANRs include:

  1. Performing disk I/O, network requests, or complex computations on the main thread
  2. Deadlock or circular dependencies between threads
  3. Main thread waiting for background work to complete via improper synchronization
  4. Executing long-running operations in a BroadcastReceiver or Service

To put the impact of ANRs in perspective, consider these statistics:

  • 84% of users say app performance and speed are important to them (source: Lab Cave study)
  • Apps with more than 2% ANR rate have 60% lower retention than apps with less than 0.5% ANR rate (source: Google Play data)
  • ANRs and crashes are the #1 reason for 1-star app reviews on the Play Store (source: Instabug report)

Clearly, ANRs have an outsized negative impact on user experience, app ratings, and overall business metrics. It‘s critical that we as developers do everything in our power to prevent them.

Proactive ANR Prevention Techniques

The best way to handle ANRs is to avoid them in the first place through careful app architecture and coding practices. Here are some key preventative techniques:

1. Keep main thread work short

Any code that runs on the main thread should complete very quickly, on the order of milliseconds. As a rule of thumb, any task that may take more than 50ms should be moved to a background thread.

This includes things like:

  • Disk I/O (reading/writing files, SharedPreferences, SQLite DBs, etc.)
  • Network requests
  • Bitmap decoding
  • Complex algorithms or data processing

The main thread should be reserved for quick, lightweight tasks directly related to the UI. Anything else risks blocking the thread and triggering an ANR.

2. Use proper threading techniques

To keep the main thread unblocked, you‘ll need to offload longer running tasks to background threads. There are a few common approaches:

  • AsyncTasks (simple one-off tasks)
  • Thread/Executor framework (more control over the threading model)
  • Kotlin coroutines (lightweight concurrency framework)
  • WorkManager (best for deferrable, long-running tasks)
  • IntentService (performs work on a background thread, creates separate worker thread)

Here‘s an example of moving a long disk I/O operation to a background coroutine:

suspend fun saveData() {
    withContext(Dispatchers.IO) {
        // Perform database write on background thread
        db.recordDao().insert(newRecord)
    }
}

By wrapping the database write in a withContext block with the Dispatchers.IO dispatcher, we ensure it will run on a background thread pool optimized for I/O tasks.

3. Be careful with synchronization

Whenever you have multiple threads accessing shared data, there‘s a risk of race conditions and deadlocks that can hang your app. Some tips for avoiding these pitfalls:

  • Minimize the scope of synchronized blocks
  • Avoid nested synchronization if possible
  • Always acquire locks in the same order to prevent deadlock
  • Prefer higher-level concurrent data structures like ConcurrentHashMap instead of manual synchronization
  • Consider using the java.util.concurrent primitives like CountdownLatch and Semaphore

Here‘s an example of safely accessing a shared counter from multiple threads using AtomicInteger:

private val counter = AtomicInteger(0)

fun incrementCount() {
    counter.incrementAndGet()
}

fun getCount(): Int {
    return counter.get()
}

The AtomicInteger class provides thread-safe access to an integer value, avoiding the need for explicit synchronization.

4. Optimize BroadcastReceivers and Services

BroadcastReceivers and Services are common sources of ANRs because they run on the main thread by default. It‘s important to keep onReceive() and onStartCommand() implementations as short as possible.

If you need to perform longer work in response to a broadcast or service command, start an IntentService or use the JobScheduler API to defer the task to a background thread.

ANR Detection and Reporting

Even the most carefully architected app can still experience the occasional ANR, especially as the codebase grows in complexity. That‘s why it‘s essential to have robust mechanisms for detecting ANRs in the wild and capturing the necessary debug info to resolve them.

Using the ANR-WatchDog library

The easiest way to detect ANRs is to integrate the open source ANR-WatchDog library in your app. It implements a watchdog timer on a background thread that periodically pings the main thread. If the main thread fails to respond within 5 seconds, ANR-WatchDog generates an incident report with key diagnostic info.

Here‘s how you set it up:

  1. Add the dependency to your module‘s build.gradle file:

    dependencies {
     implementation ‘com.github.anrwatchdog:anrwatchdog:1.4.0‘
    } 
  2. Initialize ANR-WatchDog in your Application class:

    class MyApp : Application() {
    
     override fun onCreate() {
         super.onCreate()
    
         ANRWatchDog()
             .setIgnoreDebugger(true)
             .setReportMainThreadOnly()
             .setANRListener { error ->
                 // Process the ANR error
                 MyTracker.reportEvent(error)
             }
             .start()
     }
    }

That‘s it! With just a few lines of code you get automatic ANR detection and reporting.

When an ANR occurs, the ANRListener callback will be invoked with an ANRError object containing a full stacktrace of where the main thread was stuck. You can log this data, write it to a file, or send it to your crash reporting service of choice for later analysis.

In a 2020 survey of 1000+ Android developers, over 80% reported using a crash reporting tool, with the top 3 being Crashlytics, Sentry, and Bugsnag. Integrating ANR-WatchDog with one of these services is a best practice for most production apps.

Manual ANR detection

If you prefer a more DIY approach, it‘s not too difficult to implement your own ANR detection mechanism using a simple Handler polling technique:

class ANRDetector : Thread() {

    private val handler = Handler(Looper.getMainLooper())

    @Volatile private var lastResponsiveTs = 0L

    override fun run() {
        while (!isInterrupted) {
            handler.post {
                lastResponsiveTs = SystemClock.uptimeMillis()
            }

            sleep(ANR_INTERVAL)

            if (SystemClock.uptimeMillis() - lastResponsiveTs >= ANR_TIMEOUT) {
                // UI thread has been unresponsive for at least 5 seconds
                val stacktrace = Looper.getMainLooper().thread.stackTrace
                Log.e("ANR", "Detected ANR, stacktrace: \n${stacktrace.joinToString("\n")}")
            }
        }
    }

    companion object {
        private const val ANR_TIMEOUT = 5000L
        private const val ANR_INTERVAL = 500L
    }
}

This ANRDetector thread pings the main thread every 500ms by posting a Runnable that updates a lastResponsiveTs timestamp. If this "heartbeat" timestamp isn‘t updated for more than 5 seconds, we assume the main thread is blocked and capture its stacktrace for reporting.

The advantage of this approach is you have full control over the detection parameters and reporting logic. The downside is more moving pieces to manage compared to a library.

Analyzing ANR stacktraces

Once you‘ve captured an ANR stacktrace, either manually or via a reporting tool, the next step is understanding what it‘s telling you. Here‘s an example trace:

Process: com.example.myapp
Thread: "main"
State: RUNNABLE
Stack:
  - com.example.myapp.MainFragment.refreshFeed(MainFragment.java:358)
  - com.example.myapp.MainFragment.access$200(MainFragment.java:42)
  - com.example.myapp.MainFragment$3.onClick(MainFragment.java:314)
  - android.view.View.performClick(View.java:7339)
  - android.view.View.performClickInternal(View.java:7305)
  - android.view.View.access$3200(View.java:846)
  - android.view.View$PerformClick.run(View.java:27787)
  - android.os.Handler.handleCallback(Handler.java:873)
  - android.os.Handler.dispatchMessage(Handler.java:99)
  - android.os.Looper.loop(Looper.java:214)
  - android.app.ActivityThread.main(ActivityThread.java:7356)
  - java.lang.reflect.Method.invoke(Method.java:-1)
  - com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
  - com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

The first thing to note is the thread name ("main") and its state ("RUNNABLE"). This tells us the ANR occurred on the main thread while it was actively running (as opposed to being blocked on I/O or waiting on a lock).

Next, we see the full callstack of what the thread was executing when it became unresponsive. In this case, we can see the thread was stuck inside a refreshFeed() method of the MainFragment class. This gives us a clear starting point for debugging.

Some common patterns to look for in ANR stacktraces include:

  • Methods that perform network or database operations on the main thread
  • Deeply nested synchronized blocks that could be deadlocking
  • Calls to 3rd party SDKs or libraries on the main thread
  • Custom view onDraw() or onMeasure() implementations that do expensive work
  • Blocking calls to Thread.sleep() or Object.wait() on the main thread

By carefully analyzing the stacktrace and identifying the offending code paths, you can narrow down the potential ANR causes and prioritize fixes accordingly.

ANRs by the Numbers

To further underscore the importance of proactive ANR detection and prevention, let‘s take a look at some insightful statistics from a 2021 developer survey conducted by Instabug:

ANR Frequency % of Respondents
Multiple times per day 5%
Once per day 6%
Multiple times per week 18%
Once per week 20%
Once per month 51%

As you can see, a significant portion of developers (29%) are dealing with ANRs on a daily or weekly basis. Only half of respondents said ANRs occur less than once per month in their apps.

When asked about the most common root causes of ANRs, developers reported:

ANR Cause % of Respondents
Complex calculation on main thread 61%
Slow database queries 46%
Long disk reads/writes 38%
Excessive synchronized blocks 35%
Network calls on main thread 33%

These results align with the guidance we covered earlier – keeping long-running work like database/disk I/O and complex computations off the main thread is critical for preventing ANRs.

Future of ANR Detection

As the Android framework and tooling continues to evolve, it‘s likely we‘ll see even more built-in support for ANR detection and analysis.

In Android 11, the platform introduced the getHistoricalProcessExitReasons() API for retrieving details of recent ANRs and other app termination reasons. While not a full replacement for in-app ANR detection, it provides a high-level view of process health that can be useful for monitoring trends over time.

The Android Studio team is also actively working on improving ANR tooling in the profiler. Recent releases have added the ability to import ANR traces captured from users‘ devices for analysis alongside performance data from the CPU, memory, and network profilers.

As Jingbo Zhou, tech lead for Android Studio performance tools, shared in a recent interview:

"We‘re investing heavily in making the ANR analysis experience more intuitive and actionable for developers. Our goal is to provide a unified workflow that combines data from the on-device ANR detection with the rich profiling capabilities of Android Studio. Stay tuned for more updates in this area!"

It‘s an exciting time to be an Android developer, with increasingly powerful tools at our disposal for diagnosing and resolving performance issues.

Conclusion

ANRs may be the bane of every Android developer‘s existence, but they don‘t have to ruin your app‘s user experience. By following Android performance best practices and leveraging the latest ANR detection and reporting tools, you can minimize the impact of ANRs and keep your app running smoothly.

Some key takeaways from this guide:

  1. Keep main thread work short and offload long-running tasks to background threads
  2. Be judicious with synchronization and avoid nested locking that can lead to deadlock
  3. Integrate a library like ANR-WatchDog or implement manual detection to capture ANRs in production
  4. Analyze ANR stacktraces to identify and fix the root cause, whether it‘s I/O, complex computations, or blocking calls
  5. Stay up-to-date with the latest Android profiling tools to streamline your ANR analysis workflow

As a professional Android developer, you have a responsibility to your users to provide the best possible experience. Implementing a robust ANR detection and resolution strategy is a critical part of meeting that bar. Your app‘s success depends on it!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *