ANR(ビュー)

コンセプトと Jetpack Compose の実装

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

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

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

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

  • 入力ディスパッチのタイムアウト: アプリが 5 秒以内に入力 イベント(キーの押下や画面のタップなど)に応答しなかった場合。
  • サービスの実行: アプリによって宣言されたサービスが数秒以内に Service.onCreate()Service.onStartCommand()/Service.onBind() の実行を完了できなかった場合。
  • **Service.startForeground() が呼び出されない**: アプリが Context.startForegroundService() を使用してフォアグラウンドで新しいサービスを開始したが そのサービスが 5 秒以内に startForeground() を呼び出さなかった場合。
  • インテントのブロードキャスト: BroadcastReceiver が、設定された時間内に実行を完了しなかった場合。アプリにフォアグラウンドのアクティビティがある場合、タイムアウトは 5 秒です。
  • **JobScheduler のインタラクション**: JobService が数秒以内に JobService.onStartJob() または JobService.onStopJob() から戻らない場合、 または ユーザーが開始したジョブ が開始され、 JobService.onStartJob() が呼び出されてから数秒以内にアプリが JobService.setNotification() を呼び出さない場合。Android 13 以前を対象とするアプリの場合、ANR は認識されず、アプリに報告されません。Android 14 以降を対象とするアプリでは、ANR は認識され、アプリに報告されます。

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

問題を解決する

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

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

アプリのメインスレッドが 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 フレームワークには、タスクをワーカー スレッドに移行するために役立つクラスが用意されています。詳細については、ワーカー スレッドをご覧ください。

メインスレッドの I/O

メインスレッドで I/O オペレーションを実行すると、メインスレッドでの処理が遅くなり、ANR が発生する可能性があります。Compose では、初期状態を導出する際に、ディスク読み取り(SharedPreferences やデータベース呼び出しなど)を誤ってトリガーしてしまうことがよくあります。

長時間実行される I/O オペレーションは、UI レイヤから離れた場所で実行します。ViewModelwithContext(Dispatchers.IO) を使用するか、データレイヤ で Repository を使用することをおすすめします。前のセクションで示したように、すべての IO オペレーションをワーカー スレッドに移動することをおすすめします。

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

ロックの競合

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

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

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

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

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

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

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

図 4. 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() などのメカニズムを使用して、 ワーカー スレッドとメインスレッドの間のやり取りを行います。

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

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

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

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

アプリは、onReceive メソッドで BroadcastReceiver短いオペレーションのみを実行する必要があります。ただし、ブロードキャスト メッセージの結果として、アプリがもっと複雑な 処理を必要とする場合は、タスクを ViewModel(Kotlin コルーチン、スコープ、ディスパッチャーの機能を活用)に委ねる必要があります。 タスクが数秒以内に完了すると予想される場合は、任意のタイプの状態ホルダーに委ねます。 数秒以上かかると予想される場合は、WorkManager に委ねます。

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

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

図 5. メインスレッドでの 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 タイムラインは、ワーカー スレッドに委ねられた作業を示しています。

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

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

ブロードキャスト レシーバは goAsync() を使用して、メッセージの処理に時間がかかることをシステムに通知できます。ただし、 finish()PendingResult オブジェクトで呼び出す必要があります。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 タイムアウトが適用されます。