◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Expo SDK 58 が 10/15 に正式リリースされました(Beta は 09/15 から)。上げる前に依存の対応状況を確認しましょう●SDK 58 ISSUE — Android ウィジェットが定期更新されない、Stripe が静的リンクになるなど初期報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Expo SDK 58 が 10/15 に正式リリースされました(Beta は 09/15 から)。上げる前に依存の対応状況を確認しましょう●SDK 58 ISSUE — Android ウィジェットが定期更新されない、Stripe が静的リンクになるなど初期報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-10-24中級

ANR の原因は、画面を閉じるときの「後片付け」でした。トレースから止めている行を拾って分類します

Android の ANR トレースを読み、メインスレッドを止めていた行を課金クライアントの終了や画像の同期デコードなどに分類する Python スクリプトです。壁紙アプリで見つけた二つの原因と、直し方の考え方をまとめました。

Android60ANRメインスレッドPlay Billing個人開発225トラブルシューティング94

✦ プレミアム記事

Play Console の ANR の欄に、同じような赤い行が並ぶ日があります。手元の端末では一度も再現しません。ところが低スペックの端末の画面ばかりに、応答なしが集まっています。

再現できないものは直せないと考えて、しばらく寝かせてしまった時期がありました。再現できないときに頼れるのは、端末の中身ではなく、トレースに残った「止まっていた行」です。

壁紙アプリの運用で、メインスレッドを止めていた原因を二つ見つけました。どちらも機能の本体ではなく、画面を閉じるときの後片付けと、ユーザーの目に見えない背景の準備でした。その見つけ方をスクリプトにしたものと、直した考え方をお伝えします。

ANR のトレースで、まず見るのは一行だけ

ANR は、メインスレッドが一定時間、入力などの処理を返せなかったときに記録されます。公式の説明では、入力イベントに5秒以内に応答できない場合や、ブロードキャストレシーバーが前面で10秒以内に終わらない場合が、その目安です。つまり、数秒の同期処理が一度あれば足ります。トレースには全スレッドの状態が並びますが、最初に見るのは "main" と書かれた塊の、いちばん上の数行です。

そこに自分のアプリの関数名が出ていれば、そこが止めていた場所です。出ていなければ、フレームワークの中で待っていて、その呼び出し元が自分のコードにあります。

1件ずつ読むと、同じ原因の ANR を何度も読むことになります。そこで、"main" の塊だけを取り出して、止めていた行を分類し、件数順に並べるスクリプトを作りました。

分類当たる行の例よくある出どころ
課金クライアントの終了/接続BillingClientImpl.endConnection画面の破棄時に呼んでいる
画像の同期デコードBitmapFactory.decodeStreamページ生成のたびに背景を読んでいる
SharedPreferences の同期書き込みQueuedWork.waitToFinish画面を離れるときの保存待ち
ファイル/DB の同期アクセスSQLiteDatabase.起動直後の読み込み
ネットワーク(メイン上)HttpURLConnection非同期にし忘れた通信
ロック待ちwaiting to lock別スレッドとの取り合い

分類のルールは、上の表のとおり正規表現で持たせました。自分のアプリで新しい原因が見つかったら、1行足すだけで増やせます。

分類スクリプト

anr_blockers.py という名前で置きます。スクリプトは、手元の Python 3 で実際に走らせて確かめています。 トレースの置き場所とパッケージ名だけ、ご自身のものに変えてください。

"""ANR のスタックトレース(テキスト)から、メインスレッドを止めた行を拾って分類する。
 
使い方: python3 anr_blockers.py --pkg com.example.app traces/*.txt
想定する形式: Android の ANR トレース(`"main" prio=5 tid=1 ...` で始まるスレッドの塊)。
Play Console の ANR 詳細からコピーしたスタックでも、先頭が `"main"` なら読めます。
"""
import argparse, re, sys
from collections import Counter
 
# 分類ルール: (ラベル, フレームに含まれる文字列の正規表現)。上から順に最初に当たったものを採用する。
# 自分のアプリで見つけた原因が増えたら、ここへ1行足していく。
RULES = [
    ("課金クライアントの終了/接続", re.compile(r"BillingClient(Impl)?\.(endConnection|startConnection)")),
    ("画像の同期デコード",       re.compile(r"BitmapFactory\.decode|ImageDecoder\.decode|BitmapDrawable|Drawable\.createFromPath")),
    ("SharedPreferences の同期書き込み", re.compile(r"SharedPreferencesImpl.*(commit|awaitLoadedLocked|waitToFinish)|QueuedWork\.waitToFinish")),
    ("ファイル/DB の同期アクセス", re.compile(r"java\.io\.(File|FileInputStream|FileOutputStream)\.|SQLiteDatabase\.|SQLiteConnection\.")),
    ("ネットワーク(メイン上)",   re.compile(r"java\.net\.|okhttp3\.internal\.connection|HttpURLConnection")),
    ("ロック待ち",               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):
    """トレース全体から "main" スレッドの塊だけを取り出す。"""
    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:  # 上(新しい呼び出し)から見て、最初に当たったルールを採る
        for label, rx in RULES:
            if rx.search(fr):
                return label, fr
    return "未分類", frames[0] if frames else "(フレームなし)"
 
 
def first_own_frame(frames, pkg):
    for fr in frames:
        if fr.startswith(pkg):
            return fr
    return "(自分のパッケージのフレームなし)"
 
 
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--pkg", required=True, help="自分のアプリのパッケージ名の接頭辞")
    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:                      # 読めないファイルは黙って捨てず、必ず報告する
            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"読んだトレース: {len(a.files)} 件 / main の塊: {total} 件 / 読めなかった: {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} 件  最初の自分のフレーム: {site}")
    for p in unknown:
        print(f"\n! 読めませんでした: {p}  (先頭が \"main\" prio=… で始まっているか確認)")
    sys.exit(0 if not unknown else 2)
 
 
if __name__ == "__main__":
    main()

何を解決するかというと、「同じ原因の ANR を、何度も目で読む時間」を、一回の実行に置き換えます。

なぜこう書いたかを、三つだけ書き残します。

  • "main" の塊だけを見ます。他のスレッドの状態は、原因の手がかりになることもありますが、最初の切り分けでは情報が多すぎると私は考えています。
  • ルールは上から順に、最初に当たったものを採ります。フレームは新しい呼び出しが上に来るので、いちばん上に近い原因が、実際に止めていた場所だと判断しています。
  • 読めなかったファイルは、黙って数から外さず、終了コード2と一緒に報告します。数が合わなくなる原因を、あとから探したくないからです。
✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦ANR のスタックトレースを手で一件ずつ読む代わりに、止めている行を種類別に数えて、最初に直すべき一か所を決められるようになります
✦画面を閉じるときの後片付けや、見えない背景の画像デコードが、低スペックの端末だけで応答なしを起こす理由を、自分のコードで先回りして疑えるようになります
✦「再現できないから直せない」と数週間寝かせてしまう前に、トレースの形から原因の候補を絞る手順を持てるようになります
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ 開発ツール2026-10-22
「容量不足」と出た動画の保存。空き容量を疑う前に、ファイルが 2GB を超えていないかを見ます
Android で 2.2GB の動画だけがギャラリーに保存できず、「Not enough space」と出る。空き容量は十分なのに、です。Expo の公開 Issue を読み、同じ境目をこの手元の Linux で動かして確かめました。保存前に大きさを見る数行と、分けてコピーする考え方をまとめています。
⬡ 開発ツール2026-10-21
iOS では時刻どおりに変わるウィジェットが、Android では「Initial」のまま動かない
expo-widgets の updateTimeline は Android では何も起こさず、生成される XML の updatePeriodMillis も 0 です。症状の見分け方、updateSnapshot で補える範囲、設計を切り替える判断と確認スクリプトをまとめました。
⬡ 開発ツール2026-10-07
日本語の見出しだけ太字にならない — Android で fontWeight が効かないときの切り分けと直し方
Rork で作ったアプリで、iPhone では太字なのに Android では日本語の見出しが細いままになる症状の切り分け記事です。原因の見分け方、fontFamily を重みごとに分ける直し方、確認用コードを載せています。