アプリの応答性を維持する

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

あらゆるパフォーマンス テストに合格するコードを作成することは可能ですが、それでも、反応の遅さ、ハング、フリーズが長期にわたって発生しているように感じることや、入力処理に時間がかかりすぎることがあります。アプリの応答性に関して起こり得る最悪の事態は、「アプリケーション応答なし」(ANR)のダイアログが表示されることです。

Android システムでは、図 1 のように、アプリが応答していないことを伝えるダイアログを表示することで、一定期間にわたって応答が不十分なアプリから保護します。このダイアログが表示された時点でアプリはかなりの期間応答していないため、アプリを終了するオプションがユーザーに表示されます。ANR ダイアログがユーザーに表示されないように、アプリの応答性を維持する設計が重要になります。

このドキュメントでは、アプリが応答しているかどうかを Android システムがどのように判断しているかについて説明します。また、アプリの応答性を維持するためのガイドラインも紹介します。

ANR をトリガーする要因

一般に、アプリがユーザー入力に応答しない場合に ANR が表示されます。たとえば、アプリが UI スレッド上のなんらかの I/O 操作(多くの場合、ネットワーク アクセス)をブロックすると、システムはその後のユーザー入力イベントを処理できなくなります。あるいは、アプリで複雑なメモリ内構造を構築したり、UI スレッドでゲームの次の動作を計算したりするのに膨大な時間がかかるようになるかもしれません。こうした計算を効率的に行うことは常に重要ですが、効率性がきわめて高いコードでも実行には時間がかかります。

アプリの処理時間が長くなる可能性がある状況では、UI スレッドで処理を行わないでください。代わりに、ワーカー スレッドを作成し、処理の大部分をそのスレッドで行います。これにより、(ユーザー インターフェースのイベントループを駆動する)UI スレッドが実行されたままになり、コードがフリーズしたとシステムが判断するのを防ぐことができます。通常、このようなスレッド化はクラスレベルで行われるため、応答性はクラスの問題と考えることができます(メソッドレベルの問題である基本的なコードのパフォーマンスと比べてみてください)。

Android では、アプリの応答性をアクティビティ マネージャーとウィンドウ マネージャーの各システム サービスで監視しています。Android は以下のいずれかの条件を検出すると、特定のアプリの ANR ダイアログを表示します。

  • 入力イベント(キーの押下や画面タッチなどのイベント)に対する応答が 5 秒以内にない。
  • BroadcastReceiver の実行が 10 秒以内に終了しない。

ANR を回避する方法

デフォルトでは、Android アプリは通常、完全に単一スレッド(「UI スレッド」または「メインスレッド」)で実行されます。これは、終了までに長時間かかる UI スレッドにおいては、アプリのあらゆる処理が ANR ダイアログの表示をトリガーする可能性があることを意味します。なぜなら、アプリは入力イベントやインテントのブロードキャストを処理する機会を自身には与えないためです。

そのため、UI スレッドで実行されるメソッドでは、そのスレッドで実行する処理をできる限り少なくする必要があります。特にアクティビティでは、主なライフサイクル メソッド(onCreate()onResume() など)で設定する内容をできる限り少なくしてください。実行時間が長くなる可能性がある処理(ネットワークやデータベースの処理など)や、CPU への負荷が高い計算(ビットマップのサイズ変更など)は、ワーカー スレッドで行う必要があります(あるいは、データベースの処理の場合は非同期リクエストを使用します)。

時間がかかる処理用にワーカー スレッドを作成するための最も効果的な方法は、AsyncTask クラスを使用する方法です。AsyncTask を拡張して doInBackground() メソッドを実装するだけで処理を実行できます。進行状況の変化をユーザーに送信するには、publishProgress() を呼び出します。このメソッドがさらに onProgressUpdate() コールバック メソッドを呼び出します。onProgressUpdate()(UI スレッドで実行されます)を実装することで、ユーザーへの通知が可能になります。以下に例を示します。

Kotlin

    private class DownloadFilesTask : AsyncTask<URL, Int, Long>() {

        // Do the long-running work in here
        override fun doInBackground(vararg urls: URL): Long? {
            val count: Float = urls.size.toFloat()
            var totalSize: Long = 0
            urls.forEachIndexed { index, url ->
                totalSize += Downloader.downloadFile(url)
                publishProgress((index / count * 100).toInt())
                // Escape early if cancel() is called
                if (isCancelled) return totalSize
            }
            return totalSize
        }

        // This is called each time you call publishProgress()
        override fun onProgressUpdate(vararg progress: Int?) {
            setProgressPercent(progress.firstOrNull() ?: 0)
        }

        // This is called when doInBackground() is finished
        override fun onPostExecute(result: Long?) {
            showNotification("Downloaded $result bytes")
        }
    }
    

Java

    private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
        // Do the long-running work in here
        protected Long doInBackground(URL... urls) {
            int count = urls.length;
            long totalSize = 0;
            for (int i = 0; i < count; i++) {
                totalSize += Downloader.downloadFile(urls[i]);
                publishProgress((int) ((i / (float) count) * 100));
                // Escape early if cancel() is called
                if (isCancelled()) break;
            }
            return totalSize;
        }

        // This is called each time you call publishProgress()
        protected void onProgressUpdate(Integer... progress) {
            setProgressPercent(progress[0]);
        }

        // This is called when doInBackground() is finished
        protected void onPostExecute(Long result) {
            showNotification("Downloaded " + result + " bytes");
        }
    }
    

このワーカー スレッドを実行するには、インスタンスを作成して execute() を呼び出します。

Kotlin

    DownloadFilesTask().execute(url1, url2, url3)
    

Java

    new DownloadFilesTask().execute(url1, url2, url3);
    

AsyncTask よりも複雑ですが、独自の Thread または HandlerThread クラスを作成することをおすすめします。その場合、Process.setThreadPriority() を呼び出して THREAD_PRIORITY_BACKGROUND を渡すことにより、スレッドの優先値を「background」に設定する必要があります。デフォルトではスレッドが UI スレッドと同じ優先値で動作するため、このようにスレッドの優先値を小さく設定しないと、スレッドでのアプリの処理速度が低下する可能性があります。

Thread または HandlerThread を実装する場合は、ワーカー スレッドが終了するのを待機している間、UI スレッドがブロックしないようにします。つまり、Thread.wait() または Thread.sleep() を呼び出さないようにします。ワーカー スレッドが終了するのを待機している間、ブロックする代わりに、メインスレッドで他のスレッド用の Handler を提供し、ワーカー スレッドの終了時にポストバックします。このようにアプリを設計すると、アプリの UI スレッドで入力に対する応答性を維持できるため、入力イベントのタイムアウト(5 秒)によって ANR ダイアログが表示されるのを回避することが可能です。

BroadcastReceiver の実行時間には特定の制約があるため、ブロードキャスト レシーバで実行することになっている処理、つまり、バックグラウンドでの不連続な少量の処理(設定の保存や Notification の登録など)が重要視されます。そのため、UI スレッドで呼び出される他のメソッドと同様に、実行時間が長くなる可能性があるブロードキャスト レシーバでの処理や計算をアプリで行わないようにする必要があります。ただし、実行時間が長くなる可能性がある処理をインテントのブロードキャストに対して行う必要がある場合は、ワーカー スレッドで負荷の高いタスクを実行するのではなく、アプリで IntentService を開始する必要があります。

BroadcastReceiver オブジェクトに関するもう 1 つのよくある問題は、その実行頻度が高すぎる場合に発生します。バックグラウンドで頻繁に実行すると、他のアプリで使用できるメモリの量が少なくなる可能性があります。BroadcastReceiver オブジェクトを効率的に有効および無効にする方法について詳しくは、ブロードキャスト レシーバをオンデマンドで操作するをご覧ください。

ヒント: 実行時間が長くなる可能性がある処理(ネットワークやデータベースの処理など)は、メインスレッドで誤って実行されることもありますが、StrictMode を使用することによって容易に検出できます。

応答性を強化する

一般に、アプリの処理時間が 100~200 ミリ秒を超えると、ユーザーが遅いと感じるようになります。ここでは、ANR を回避し、アプリの応答性を維持するために必要なこと以外のおすすめの方法をいくつかご紹介します。

  • アプリがユーザー入力に対する処理をバックグラウンドで行っている場合は、(UI で ProgressBar などを使用して)処理が行われていることを表示します。
  • 特にゲームの場合は、ワーカー スレッドで動作の計算を行います。
  • アプリの初期設定フェーズに時間がかかる場合は、スプラッシュ画面を表示するか、メインビューをできる限り早くレンダリングすることを検討してください。また、読み込みが進行中であることを示し、情報を非同期で表示するようにします。いずれにしても、アプリがフリーズしているとユーザーが認識しないよう、処理が行われていることをなんらかの方法で示す必要があります。
  • アプリの応答性に関するボトルネックを特定するには、SystraceTraceview などのパフォーマンス ツールを使用します。