◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-10-24Intermediate

The ANR Was Coming From Cleanup When a Screen Closed: Classifying the Blocking Line From Traces

A Python script that reads Android ANR traces and classifies the line that blocked the main thread, such as billing client teardown or synchronous image decoding. Two causes I found in my wallpaper apps, and the thinking behind the fixes.

Android60ANRmain threadPlay Billingindie developer47troubleshooting75

✦ Premium Article

Some days the ANR section in Play Console fills with the same red rows. I never see them on my own devices. Yet the not-responding reports gather on low-end phones.

If I cannot reproduce it, I cannot fix it. That was my reasoning, and I let one of these sit for a while. When you cannot reproduce a problem, what you can rely on is not the device but the line the trace says was blocking.

While running my wallpaper apps as an indie developer, I found two causes that were blocking the main thread. Neither was the feature itself. One was cleanup when a screen closed, the other was preparing a background the user can barely see. Here is the script I use to find them, and how I thought about the fixes.

In an ANR trace, look at one line first

An ANR is recorded when the main thread cannot get back to input handling within a set time. Per the official description, the rough thresholds are 5 seconds without responding to an input event, or a broadcast receiver running longer than 10 seconds in the foreground. A single synchronous task of a few seconds is enough. The trace lists every thread, but the first place to look is the top few lines of the block labelled "main".

If a function from your own app appears there, that is where it was stuck. If not, it was waiting inside the framework, and the caller is somewhere in your code.

Reading traces one at a time means reading the same cause over and over. So I wrote a script that pulls out only the "main" block, classifies the blocking line, and sorts by count.

CategoryExample lineUsual source
Billing client teardown/connectBillingClientImpl.endConnectionCalled when a screen is destroyed
Synchronous image decodeBitmapFactory.decodeStreamA background loaded on every page build
Synchronous SharedPreferences writeQueuedWork.waitToFinishWaiting for a save when leaving a screen
Synchronous file/DB accessSQLiteDatabase.A load right after launch
Network on the main threadHttpURLConnectionA call that was never made async
Lock waitwaiting to lockContention with another thread

The rules are regular expressions, as in the table. When you find a new cause in your own app, adding one line extends them.

The classification script

Save it as anr_blockers.py. I ran this script on Python 3 here to confirm it works. Change only the trace folder and the package name to yours.

"""Read ANR stack traces (plain text) and classify the line that blocked the main thread.
 
Usage: python3 anr_blockers.py --pkg com.example.app traces/*.txt
Expected format: Android ANR traces (thread blocks starting with `"main" prio=5 tid=1 ...`).
Stacks copied from the Play Console ANR detail also work if they start with `"main"`.
"""
import argparse, re, sys
from collections import Counter
 
# Rules: (label, regex matched against a frame). The first rule that matches wins.
# When you find a new cause in your own app, add one line here.
RULES = [
    ("Billing client teardown/connect", re.compile(r"BillingClient(Impl)?\.(endConnection|startConnection)")),
    ("Synchronous image decode",       re.compile(r"BitmapFactory\.decode|ImageDecoder\.decode|BitmapDrawable|Drawable\.createFromPath")),
    ("Synchronous SharedPreferences write", re.compile(r"SharedPreferencesImpl.*(commit|awaitLoadedLocked|waitToFinish)|QueuedWork\.waitToFinish")),
    ("Synchronous file/DB access", re.compile(r"java\.io\.(File|FileInputStream|FileOutputStream)\.|SQLiteDatabase\.|SQLiteConnection\.")),
    ("Network on the main thread",   re.compile(r"java\.net\.|okhttp3\.internal\.connection|HttpURLConnection")),
    ("Lock wait",               re.compile(r"waiting to lock|Object\.wait|LockSupport\.park")),
]
 
THREAD_HEAD = re.compile(r'^"(?P<name>[^"]+)"\s+prio=\d+')
FRAME = re.compile(r"^\s+(at\s+)?(?P<fq>[\w.$<>]+)\((?P<src>[^)]*)\)")
 
 
def main_blocks(text):
    """Pull out only the "main" thread blocks from the whole trace."""
    out, cur = [], None
    for line in text.splitlines():
        m = THREAD_HEAD.match(line)
        if m:
            if cur is not None:
                out.append(cur)
            cur = [] if m.group("name") == "main" else None
            continue
        if cur is not None:
            cur.append(line)
    if cur is not None:
        out.append(cur)
    return out
 
 
def frames_of(block):
    out = []
    for line in block:
        f = FRAME.match(line)
        if f:
            out.append(f.group("fq") + "(" + f.group("src") + ")")
        elif "waiting to lock" in line or "waiting on" in line:
            out.append(line.strip())
    return out
 
 
def classify(frames):
    for fr in frames:  # Newest call is on top; take the first rule that matches
        for label, rx in RULES:
            if rx.search(fr):
                return label, fr
    return "Unclassified", frames[0] if frames else "(no frames)"
 
 
def first_own_frame(frames, pkg):
    for fr in frames:
        if fr.startswith(pkg):
            return fr
    return "(no frame from your package)"
 
 
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--pkg", required=True, help="prefix of your own app package name")
    ap.add_argument("files", nargs="+")
    a = ap.parse_args()
 
    by_label, by_site, unknown = Counter(), Counter(), []
    for path in a.files:
        text = open(path, encoding="utf-8", errors="replace").read()
        blocks = main_blocks(text)
        if not blocks:                      # Never drop an unreadable file silently; always report it
            unknown.append(path)
            continue
        for b in blocks:
            fr = frames_of(b)
            label, hit = classify(fr)
            by_label[label] += 1
            by_site[(label, first_own_frame(fr, a.pkg))] += 1
 
    total = sum(by_label.values())
    print(f"Traces read: {len(a.files)} / main blocks: {total} / unreadable: {len(unknown)}")
    for label, n in by_label.most_common():
        print(f"\n[{label}] {n}")
        for (lb, site), c in by_site.most_common():
            if lb == label:
                print(f"   {c:>3}  first own frame: {site}")
    for p in unknown:
        print(f"\n! Could not read: {p}  (check that it starts with \"main\" prio=...)")
    sys.exit(0 if not unknown else 2)
 
 
if __name__ == "__main__":
    main()

What it solves: the time spent reading the same ANR by eye, replaced by one run.

Three choices worth writing down.

  • It looks only at the "main" block. Other threads can hold clues, but for a first triage they are too much information.
  • Rules are applied top to bottom and the first match wins. The newest call is at the top of a stack, so the match nearest the top is the place that was actually blocking.
  • Files it cannot read are reported with exit code 2, not silently dropped. I do not want to hunt for why the counts do not add up later.
✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You can stop reading ANR stack traces one by one, count the blocking lines by type instead, and decide which single place to fix first
✦You can suspect, ahead of time, the cleanup on screen close and the image decode behind an invisible background that cause ANRs only on low-end devices
✦You can narrow down likely causes from the shape of the trace, instead of letting a bug sit for weeks because you cannot reproduce it
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-10-22
A Video Fails to Save with "Not Enough Space" — Check Whether the File Is Over 2GB Before Blaming Storage
On Android, a 2.2GB video won't save to the gallery and the error says "Not enough space", even with plenty of free storage. I read the public Expo issue and reproduced the same boundary on Linux. Here is a size check to add before saving, and the idea of copying in pieces.
⬡ Dev Tools2026-10-07
Japanese Headings Won't Turn Bold on Android: How to Trace and Fix a fontWeight That Does Nothing
A troubleshooting note for Rork apps where headings look bold on iPhone but stay thin on Android. How to tell the symptoms apart, pick weights through fontFamily, and verify the fix.
⬡ Dev Tools2026-05-15
Rork Max Android App Crashes Only on Older OS Versions: Java 8 Desugaring Fix
Diagnose why your Rork Max Android app crashes only on older OS versions (Android 6–8) and fix it permanently with Java 8 core library desugaring. Includes the two cases where the fix alone was not enough.