ANR

Android アプリの UI スレッドが長時間ブロックされると、アプリケーション応答なし(ANR)エラーが発生します。アプリがフォアグラウンドにある場合、図 1 に示すように、ユーザーにダイアログが表示されます。ANR ダイアログにより、ユーザーはアプリを強制終了できます。

図 1. ユーザーに表示された ANR ダイアログ

図 1. ユーザーに表示された ANR ダイアログ

ANR が問題なのは、UI を更新する役割を担うアプリのメインスレッドがユーザー入力イベントの処理や描画を行うことができず、ユーザーに不満を感じさせるからです。アプリのメインスレッドの詳細については、プロセスとスレッドをご覧ください。

次のいずれかの状況が発生すると、アプリで ANR がトリガーされます。

  • 入力ディスパッチのタイムアウト: アプリが 5 秒以内に入力イベント(キーの押下や画面のタップなど)に応答しなかった場合。
  • サービスの実行: アプリによって宣言されたサービスが数秒以内に Service.onCreate()Service.onStartCommand() / Service.onBind() の実行を完了できなかった場合。
  • Service.startForeground() が呼び出されない: アプリが Context.startForegroundService() を使用してフォアグラウンドで新しいサービスを開始したが、そのサービスが 5 秒以内に startForeground() を呼び出さなかった場合。
  • インテントのブロードキャスト: BroadcastReceiver が、設定された時間内に実行を完了しなかった場合。アプリにフォアグラウンドのアクティビティがある場合、タイムアウトは 5 秒です。

アプリで ANR が発生する場合、この記事のガイダンスが問題の診断と解決に役立ちます。

問題を検出する

アプリをすでに公開している場合は、Android Vitals を使用してアプリの ANR に関する情報を確認できます。本番環境では他のツールを使用して ANR を検出することもできますが、サードパーティ ツールは、Android Vitals と異なり、古いバージョンの Android(Android 10 以下)では ANR をレポートできません。

Android Vitals

Android Vitals は、アプリの ANR 発生率をモニターして改善するために役立ちます。Android Vitals は、以下の ANR 発生率を測定します。

  • ANR 発生率: 1 日のアクティブ ユーザーにつき、いずれかのタイプの ANR が発生した割合。
  • ユーザーが認識した ANR 発生率: 1 日のアクティブ ユーザーにつき、ユーザーが認識した ANR が少なくとも 1 回発生した割合。現在、ユーザーが認識する ANR は Input dispatching timed out タイプの ANR だけです。
  • 複数回 ANR 発生率: 1 日のアクティブ ユーザーにつき、ANR が少なくとも 2 回発生した割合。

1 日のアクティブ ユーザーとは、1 日に 1 台のデバイスでアプリを使用するユニーク ユーザーを意味します。複数のセッションでアプリを使用する場合も含みます。1 人のユーザーが 1 日に複数のデバイスでアプリを使用する場合は、デバイスごとにその日のアクティブ ユーザーとしてカウントされます。複数のユーザーが 1 日に同じデバイスを使用する場合は、1 人のアクティブ ユーザーとしてカウントされます。

ユーザーが認識した ANR 発生率は「主な指標」の一つです。つまり、Google Play におけるアプリの見つけやすさに影響します。この指標は重要です。なぜなら、この指標でカウントされる ANR は、ユーザーがアプリを使用しているときに発生するものであり、重大な使用中断の原因となるからです。

Google Play では、この指標について 2 つの不正な動作のしきい値を定義しています。

  • 全体的な不正な動作のしきい値: すべてのデバイスモデルで 1 日のアクティブ ユーザーが認識した ANR 発生率が 0.47% 以上。
  • デバイスごとの不正な動作のしきい値: 1 つのデバイスモデルで 1 日のアクティブ ユーザーが認識した ANR 発生率が 8% 以上。

アプリが全体的な不正な動作のしきい値を超えると、すべてのデバイスでアプリの見つけやすさが低下する可能性があります。一部のデバイスでアプリがデバイスごとの不正な動作のしきい値を超えると、それらのデバイスでアプリの見つけやすさが低下し、ストアの掲載情報に警告が表示される可能性があります。

Android Vitals を使用すると、アプリで ANR が過度に発生するときに、Google Play Console を介してアラートを受け取ることができます。

Google Play が Android Vitals のデータを収集する方法については、Google Play Console のドキュメントをご覧ください。

ANR を診断する

ANR を診断する際は、次のような一般的なパターンを探します。

  • メインスレッドで I/O に関連するアプリの処理が遅くなっている。
  • メインスレッドでアプリの計算に長時間かかっている。
  • メインスレッドが別のプロセスへの同期バインダー呼び出しを行っており、その別のプロセスが復帰するまでに長時間かかっている。
  • メインスレッドが、別のスレッドで発生している長時間実行オペレーションの同期ブロックを待機してブロックされている。
  • メインスレッドが、プロセス内で、またはバインダー呼び出しによって、別のスレッドとデッドロック状態になっている。メインスレッドは、単に長時間実行オペレーションが終了するのを待機しているのでなく、デッドロック状態になっています。詳細については、Wikipedia のデッドロックをご覧ください。

ANR の原因を特定するには、次の手法が有用です。

HealthStats

HealthStats は、ユーザーおよびシステムの合計時間、CPU 時間、ネットワーク、無線データ、画面のオン / オフ時間、ウェイクアップ アラームをキャプチャして、アプリケーションの健全性に関する指標を提供します。これは、全体的な CPU 使用率とバッテリーの消耗の測定で役立ちます。

デバッグ

Debug を使用すると、開発中の Android アプリを検査できます。これには、トレースや割り当てのカウントでアプリ内のジャンクや遅延を特定することなどが含まれます。Debug を使用して、ランタイム メモリとネイティブ メモリのカウンタ、および特定のプロセスのメモリ フットプリントを特定する際に役立つメモリ指標を取得することもできます。

ApplicationExitInfo

ApplicationExitInfo は Android 11(API レベル 30)以降で使用でき、アプリの終了の理由に関する情報を提供します。これには、ANR、メモリの不足、アプリのクラッシュ、CPU の過剰な使用、ユーザーの中断、システムの中断、実行時の権限の変更などが含まれます。

厳格モード

StrictMode を使用すると、アプリの開発中にメインスレッド上の誤った I/O オペレーションを見つけやすくなります。StrictMode は、アプリレベルまたはアクティビティ レベルで使用できます。

バックグラウンド ANR ダイアログを有効にする

Android では、デバイスの [開発者向けオプション] で [すべての ANR を表示] が有効になっている場合にのみ、ブロードキャスト メッセージの処理に時間がかかりすぎるアプリの ANR ダイアログが表示されます。そのため、必ずしもバックグラウンド ANR ダイアログがユーザーに表示されなくても、パフォーマンスの問題がアプリで引き続き発生する可能性があります。

Traceview

Traceview を使用すると、ユースケースを調べて実行中のアプリのトレースを取得し、メインスレッドがビジー状態になっている場所を特定できます。Traceview の使用方法については、Traceview と dmtracedump によるプロファイリングをご覧ください。

トレースファイルを pull する

Android では、ANR が発生するとトレース情報が保存されます。以前の OS リリースでは、デバイスに /data/anr/traces.txt ファイルが 1 つあります。最新の OS リリースでは、複数の /data/anr/anr_* ファイルがあります。デバイスまたはエミュレータから ANR トレースにアクセスするには、ルートで Android Debug Bridge(adb)を使用します。

adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>

デバイスの [バグレポートを取得] 開発向けオプション、または開発マシンの adb bugreport コマンドを使用すると、物理デバイスからバグレポートを取得できます。詳細については、バグレポートのキャプチャと確認をご覧ください。

問題を解決する

問題を特定したら、このセクションのヒントを使用してよくある問題を解決できます。

メインスレッドのコードが遅い

アプリのメインスレッドが 5 秒を超えてビジー状態になっているコード内の場所を特定します。アプリ内の疑わしいユースケースを探し、ANR を再現してみます。

たとえば図 2 は、メインスレッドが 5 秒を超えてビジー状態になっている Traceview タイムラインを示しています。

図 2. ビジー状態のメインスレッドを示す Traceview タイムライン

図 2. ビジー状態のメインスレッドを示す Traceview タイムライン

図 2 は、問題のあるコードのほとんどが onClick(View) ハンドラで発生していることを示しています。そのようなコードの例を次に示します。

Kotlin

override fun onClick(v: View) {
    // This task runs on the main thread.
    BubbleSort.sort(data)
}

Java

@Override
public void onClick(View view) {
    // This task runs on the main thread.
    BubbleSort.sort(data);
}

この場合、メインスレッドで実行する作業をワーカー スレッドに移動する必要があります。Android フレームワークには、タスクをワーカー スレッドに移行するために役立つクラスが用意されています。詳細については、ワーカー スレッドをご覧ください。

メインスレッドの IO

メインスレッドで IO オペレーションを実行すると、メインスレッドでの処理が遅くなり、ANR が発生する可能性があります。前のセクションで示したように、すべての IO オペレーションをワーカー スレッドに移動することをおすすめします。

IO オペレーションの例としては、ネットワーク オペレーションとストレージ オペレーションがあります。詳細については、ネットワーク オペレーションの実行データの保存をご覧ください。

ロックの競合

場合によっては、ANR の原因となる作業がアプリのメインスレッドで直接実行されていないこともあります。メインスレッドが作業を完了するために必要なリソースのロックをワーカー スレッドが保持している場合に、ANR が発生することがあります。

たとえば図 4 は、ほとんどの作業がワーカー スレッドで実行される Traceview タイムラインを示しています。

図 4. ワーカー スレッドで実行中の作業を示す Traceview タイムライン

図 4. ワーカー スレッドで実行中の作業を示す Traceview タイムライン

しかし、ANR が依然として発生する場合は、Android Device Monitor でメインスレッドのステータスを確認する必要があります。一般的に、UI を更新する準備ができていて、ほとんどの場合に応答可能であれば、メインスレッドは RUNNABLE 状態です。

しかし、実行を再開できないのであれば、メインスレッドは BLOCKED 状態であり、イベントに応答できません。図 5 に示すように、Android Device Monitor では状態が 「Monitor」または「Wait」として表示されます。

図 5. Monitor ステータスのメインスレッド

図 5. Monitor ステータスのメインスレッド

次のトレースは、リソースを待機してブロック状態にあるアプリのメインスレッドを示しています。

...
AsyncTask #2" prio=5 tid=18 Runnable
  | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
  | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
  | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
  | stack=0x94a7e000-0x94a80000 stackSize=1038KB
  | held mutexes= "mutator lock"(shared held)
  at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
  - locked <0x083105ee> (a java.lang.Boolean)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
  at android.os.AsyncTask$2.call(AsyncTask.java:305)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
  at java.lang.Thread.run(Thread.java:761)
...

トレースを確認すると、メインスレッドをブロックしているコードを見つけるのに役立ちます。次のコードは、前のトレースでメインスレッドをブロックするロックを保持している原因です。

Kotlin

override fun onClick(v: View) {
    // The worker thread holds a lock on lockedResource
    LockTask().execute(data)

    synchronized(lockedResource) {
        // The main thread requires lockedResource here
        // but it has to wait until LockTask finishes using it.
    }
}

class LockTask : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? =
            synchronized(lockedResource) {
                // This is a long-running operation, which makes
                // the lock last for a long time
                BubbleSort.sort(params[0])
            }
}

Java

@Override
public void onClick(View v) {
    // The worker thread holds a lock on lockedResource
   new LockTask().execute(data);

   synchronized (lockedResource) {
       // The main thread requires lockedResource here
       // but it has to wait until LockTask finishes using it.
   }
}

public class LockTask extends AsyncTask<Integer[], Integer, Long> {
   @Override
   protected Long doInBackground(Integer[]... params) {
       synchronized (lockedResource) {
           // This is a long-running operation, which makes
           // the lock last for a long time
           BubbleSort.sort(params[0]);
       }
   }
}

もう 1 つの例は、次のコードに示すように、ワーカー スレッドからの結果を待機しているアプリのメインスレッドです。なお、同時実行を処理する独自のメカニズムを持つ Kotlin では、wait()notify() を使用する方法はおすすめできません。Kotlin を使用する場合は、可能であれば Kotlin 固有のメカニズムを使用する必要があります。

Kotlin

fun onClick(v: View) {
    val lock = java.lang.Object()
    val waitTask = WaitTask(lock)
    synchronized(lock) {
        try {
            waitTask.execute(data)
            // Wait for this worker thread’s notification
            lock.wait()
        } catch (e: InterruptedException) {
        }
    }
}

internal class WaitTask(private val lock: java.lang.Object) : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        synchronized(lock) {
            BubbleSort.sort(params[0])
            // Finished, notify the main thread
            lock.notify()
        }
    }
}

Java

public void onClick(View v) {
   WaitTask waitTask = new WaitTask();
   synchronized (waitTask) {
       try {
           waitTask.execute(data);
           // Wait for this worker thread’s notification
           waitTask.wait();
       } catch (InterruptedException e) {}
   }
}

class WaitTask extends AsyncTask<Integer[], Integer, Long> {
   @Override
   protected Long doInBackground(Integer[]... params) {
       synchronized (this) {
           BubbleSort.sort(params[0]);
           // Finished, notify the main thread
           notify();
       }
   }
}

メインスレッドをブロックする可能性のある状況は他にもいくつかあります。たとえば、LockSemaphore、リソースプール(データベース接続のプールなど)、またはその他の相互排他(mutex)メカニズムを使用するスレッドなどです。

一般的に、アプリがリソースに対して保持しているロックは評価の対象にする必要がありますが、ANR を回避するには、メインスレッドが必要とするリソースに対して保持されているロックを確認する必要があります。

ロックが保持される時間が最小限であることを確認します。さらに、そもそもアプリが保持を必要としているかどうかを評価することも重要です。ワーカー スレッドの処理に基づいて UI を更新するタイミングを決定するためにロックを使用している場合は、onProgressUpdate()onPostExecute() などのメカニズムを使用して、ワーカー スレッドとメインスレッドの間のやり取りを行います。

デッドロック

お互いに相手の保持するリソースを必要とする 2 つのスレッドがあり、スレッドが待機状態に入ると、デッドロックが発生します。アプリのメインスレッドがこの状況にある場合、ANR が発生する可能性があります。

デッドロックはコンピュータ サイエンスでよく研究されている現象で、デッドロックを回避するために使用できるデッドロック防止アルゴリズムがあります。

詳細については、Wikipedia のデッドロックデッドロック防止アルゴリズムをご覧ください。

ブロードキャスト レシーバが遅い

アプリでブロードキャスト レシーバを使用すると、機内モードの有効化 / 無効化や接続状態の変更など、ブロードキャスト メッセージに応答できます。アプリでブロードキャスト メッセージの処理に時間がかかりすぎると、ANR が発生します。

ANR は次の場合に発生します。

  • かなりの時間が経過してもブロードキャスト レシーバが onReceive() メソッドの実行を完了しなかった。
  • ブロードキャスト レシーバが goAsync() を呼び出したが、PendingResult オブジェクトで finish() の呼び出しに失敗した。

アプリが BroadcastReceiveronReceive() メソッドで短いオペレーションのみを実行するようにする必要があります。ただし、ブロードキャスト メッセージの結果として、アプリがもっと複雑な処理を必要とする場合は、IntentService にタスクを委ねる必要があります。

Traceview などのツールを使用すると、アプリのメインスレッドで長時間実行オペレーションをブロードキャスト レシーバが実行しているかどうかを確認できます。たとえば図 6 は、メインスレッドで約 100 秒間メッセージを処理するブロードキャスト レシーバのタイムラインを示しています。

図 6. メインスレッド上の BroadcastReceiver の作業を示す Traceview タイムライン

図 6. メインスレッド上の BroadcastReceiver の作業を示す Traceview タイムライン

この動作は、次の例に示すように、BroadcastReceiveronReceive() メソッドで長時間実行オペレーションを実行したことが原因で発生する可能性があります。

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    // This is a long-running operation
    BubbleSort.sort(data)
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // This is a long-running operation
    BubbleSort.sort(data);
}

このような状況では、長時間実行オペレーションを IntentService に移動することをおすすめします。そうすれば、ワーカー スレッドを使用して作業が実行されるからです。次のコードは、IntentService を使用して長時間実行オペレーションを処理する方法を示しています。

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    Intent(context, MyIntentService::class.java).also { intentService ->
        // The task now runs on a worker thread.
        context.startService(intentService)
    }
}

class MyIntentService : IntentService("MyIntentService") {
    override fun onHandleIntent(intent: Intent?) {
        BubbleSort.sort(data)
    }
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // The task now runs on a worker thread.
    Intent intentService = new Intent(context, MyIntentService.class);
    context.startService(intentService);
}

public class MyIntentService extends IntentService {
   @Override
   protected void onHandleIntent(@Nullable Intent intent) {
       BubbleSort.sort(data);
   }
}

IntentService を使用すると、長時間実行オペレーションはメインスレッドではなくワーカー スレッドで実行されます。図 7 の Traceview タイムラインは、ワーカー スレッドに委ねられた作業を示しています。

図 7. ワーカー スレッドで処理されたブロードキャスト メッセージを示す Traceview タイムライン

図 7. ワーカー スレッドで処理されたブロードキャスト メッセージを示す Traceview タイムライン

ブロードキャスト レシーバは、goAsync() を使用して、メッセージの処理に時間が必要であることをシステムに通知できます。ただし、PendingResult オブジェクトで finish() を呼び出す必要があります。次の例は、finish() を呼び出してシステムにブロードキャスト レシーバをリサイクルさせることで ANR を回避する方法を示しています。

Kotlin

val pendingResult = goAsync()

object : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        // This is a long-running operation
        BubbleSort.sort(params[0])
        pendingResult.finish()
        return 0L
    }
}.execute(data)

Java

final PendingResult pendingResult = goAsync();
new AsyncTask<Integer[], Integer, Long>() {
   @Override
   protected Long doInBackground(Integer[]... params) {
       // This is a long-running operation
       BubbleSort.sort(params[0]);
       pendingResult.finish();
   }
}.execute(data);

しかし、ブロードキャストがバックグラウンドで実行されている場合は、コードを遅いブロードキャスト レシーバから別のスレッドに移動して goAsync() を使用しても、ANR は解決しません。依然として ANR タイムアウトが適用されます。

GameActivity

GameActivity ライブラリにより、C または C++ で記述されたゲームとアプリの事例研究で ANR が削減されました。既存のネイティブ アクティビティを GameActivity に置き換えると、UI スレッドのブロックを減らし、ANR の発生をある程度防ぐことができます。

ANR の詳細については、アプリの応答性を維持するをご覧ください。スレッドの詳細については、スレッド化のパフォーマンスをご覧ください。