Android アプリの UI スレッドが長時間ブロックされると、アプリケーション応答なし(ANR)エラーが発生します。アプリがフォアグラウンドにある場合、図 1 に示すように、ユーザーにダイアログが表示されます。ANR ダイアログにより、ユーザーはアプリを強制終了できます。
図 1.ユーザーに表示された ANR ダイアログ
UI の更新を担当するアプリのメインスレッドがユーザー入力イベントを処理または描画できず、ユーザーに不満が生じるため、ANR は問題です。アプリのメインスレッドの詳細については、プロセスとスレッドをご覧ください。
次のいずれかの状況が発生すると、アプリの ANR がトリガーされます。
- アクティビティがフォアグラウンドにある場合に、アプリが 5 秒以内に入力イベントまたは
BroadcastReceiver
(キーの押下やタップイベントなど)に応答しなかった。 - フォアグラウンドのアクティビティがない場合に、かなりの時間が経過しても
BroadcastReceiver
の実行が終了しなかった。
アプリで ANR が発生する場合、このページのガイダンスが問題の診断と解決に役立ちます。
問題の検出と診断
Android には、アプリに問題があることを通知する手段と、診断を手助けする手段が用意されています。 アプリをすでに公開している場合、Android Vitals は問題の発生を警告し、診断ツールは問題の発見に役立ちます。
Android Vitals
Android Vitals は、アプリで頻繁に ANR が発生する場合に Play Console を介して警告を発することで、アプリのパフォーマンスの改善をサポートします。 Android Vitals は、アプリが次の状態のときに、ANR が多すぎると判断します。
- 1 日のセッションの 0.47% 以上で、ANR が 1 回以上発生する。
- 1 日のセッションの 0.24% 以上で、ANR が 2 回以上発生する。
1 日のセッションとは、アプリが使用された 1 日を意味します。
Google Play が Android Vitals のデータを収集する方法については、Play Console のドキュメントをご覧ください。
ANR の診断
ANR の診断では、一般的に次のようなパターンを確認します。
- メインスレッドで I/O に関連するアプリの処理が遅くなっている。
- メインスレッドでアプリによる計算が長時間になっている。
- メインスレッドが別のプロセスに同期バインダー呼び出しを行っており、その別のプロセスが復帰するまでに時間がかかっている。
- メインスレッドが、別のスレッドで発生している長時間のオペレーションの同期ブロックを待機してブロックされている。
- メインスレッドが、プロセス内またはバインダー呼び出しを介して、別のスレッドとデッドロック状態になっている。メインスレッドは、長時間のオペレーションが終了するのを待機しているだけでなく、デッドロック状態になっています。詳細については、Wikipedia のデッドロックをご覧ください。
次の手法は、このうちどれが ANR の原因となっているのかを特定するために役立ちます。
厳格モード
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 に示された問題のコードのほとんどが発生している 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 フレームワークには、タスクをワーカー スレッドに移行するために役立つクラスが用意されています。詳細については、スレッド化のヘルパークラスをご覧ください。次のコードは、AsyncTask
ヘルパークラスを使用してワーカー スレッドでタスクを処理する方法を示しています。
Kotlin
override fun onClick(v: View) { // The long-running operation is run on a worker thread object : AsyncTask<Array<Int>, Int, Long>() { override fun doInBackground(vararg params: Array<Int>): Long? = BubbleSort.sort(params[0]) }.execute(data) }
Java
@Override public void onClick(View view) { // The long-running operation is run on a worker thread new AsyncTask<Integer[], Integer, Long>() { @Override protected Long doInBackground(Integer[]... params) { BubbleSort.sort(params[0]); } }.execute(data); }
Traceview は、図 3 に示すように、ほとんどのコードがワーカー スレッドで実行されることを示します。メインスレッドは、ユーザー イベントに応答するために使用できます。
図 3. ワーカー スレッドによる処理を示す Traceview タイムライン
メインスレッドの IO
メインスレッドで IO オペレーションを実行すると、メインスレッドでの処理が遅くなり、ANR が発生する可能性があります。前のセクションで示したように、すべての IO オペレーションをワーカー スレッドに移動することをおすすめします。
IO オペレーションの例としては、ネットワーク オペレーションとストレージ オペレーションがあります。詳細については、ネットワーク オペレーションの実行とデータの保存をご覧ください。
ロックの競合
場合によっては、ANR の原因となる作業がアプリのメインスレッドで直接実行されていないこともあります。メインスレッドが作業を完了するために必要なリソースのロックをワーカー スレッドが保持している場合に、ANR が発生することがあります。
たとえば図 4 は、ほとんどの作業がワーカー スレッドで実行される Traceview タイムラインを示しています。
図 4. ワーカー スレッドで実行中の作業を示す Traceview タイムライン
ただし、ANR が依然として発生している場合は、Android Device Monitor でメインスレッドのステータスを確認する必要があります。通常、UI を更新する準備ができていて、ほとんどの場合に応答するのであれば、メインスレッドは RUNNABLE
状態です。
しかし、メインスレッドが実行を再開できない場合は BLOCKED
状態であり、イベントに応答できません。図 5 に示すように、ステータスは Android Device Monitor に Monitor または Wait として表示されます。
図 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(); } } }
Lock
や Semaphore
を使用するスレッド、リソースプール(データベース接続のプールなど)、その他の相互排他(mutex)メカニズムを含め、メインスレッドをブロックする可能性のある状況は他にもあります。
一般的に、アプリがリソースに対して保持しているロックを評価する必要がありますが、ANR を回避する場合は、メインスレッドで必要なリソースに対して保持されているロックを確認する必要があります。
ロックの保持時間が最小限であることを確認します。さらに、そもそもアプリが保持を必要としているかどうかを評価することが重要です。ロックを使用して、ワーカー スレッドの処理に基づいて UI をいつ更新するかを決定する場合は、onProgressUpdate()
や onPostExecute()
などのメカニズムを使用して、ワーカー スレッドとメインスレッドの間でやり取りします。
デッドロック
お互いに相手の保持するリソースを必要とする 2 つのスレッドがあり、スレッドが待機状態に入ると、デッドロックが発生します。アプリのメインスレッドがこの状況にある場合、ANR が発生する可能性があります。
デッドロックはコンピュータ サイエンスでよく研究されている現象で、デッドロックを回避するために使用できるデッドロック防止アルゴリズムがあります。
詳細については、Wikipedia のデッドロックとデッドロック防止アルゴリズムをご覧ください。
ブロードキャスト レシーバが遅い
アプリでブロードキャスト レシーバを使用すると、機内モードの有効化 / 無効化や接続状態の変更など、ブロードキャスト メッセージに応答できます。アプリでブロードキャスト メッセージの処理に時間がかかりすぎると、ANR が発生します。
ANR は次の場合に発生します。
- かなりの時間が経過してもブロードキャスト レシーバが
onReceive()
メソッドの実行を完了していない。 - ブロードキャスト レシーバが
goAsync()
を呼び出し、PendingResult
オブジェクトのfinish()
を呼び出せない。
アプリが BroadcastReceiver
の onReceive()
メソッドで短いオペレーションのみ行うようにします。ただし、ブロードキャスト メッセージの結果としてアプリがより複雑な処理を必要とする場合は、タスクを IntentService
に延期する必要があります。
Traceview などのツールを使用すると、アプリのメインスレッドで長時間実行オペレーションをブロードキャスト レシーバが実行しているかどうかを確認できます。たとえば図 6 は、メインスレッドで約 100 秒間メッセージを処理するブロードキャスト レシーバのタイムラインを示しています。
図 6. メインスレッドでの BroadcastReceiver の動作を示す Traceview タイムライン
この動作は、次の例に示すように、BroadcastReceiver
の onReceive()
メソッドで長時間実行オペレーションを実行することで発生する可能性があります。
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 タイムライン
ブロードキャスト レシーバは 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 タイムアウトが適用されます。
ANR の詳細については、アプリの応答性を維持するをご覧ください。スレッドの詳細については、スレッド化のパフォーマンスをご覧ください。