6 月 3 日の「#Android11: The Beta Launch Show」にぜひご参加ください。

スレッド化によるパフォーマンスの向上

Android ではスレッドを上手に使うことで、アプリのパフォーマンスを向上させることができます。このページでは、スレッドの使用について、次の側面から説明します。UI スレッド(メインスレッド)の使い方、アプリのライフサイクルとスレッドの優先順位の関係、プラットフォームに用意されているスレッドの複雑さに対処するための方法です。このページでは、それぞれの側面について、陥る可能性のある落とし穴とその回避策を説明します。

メインスレッド

ユーザーがアプリを起動すると、Android は、実行スレッドとともに新しい Linux プロセスを作成します。このメインスレッドは、UI スレッドとも呼ばれ、画面上で発生するすべての処理を実行します。メインスレッドの動作を理解すれば、最高のパフォーマンスを実現するアプリを設計できます。

内部動作

メインスレッドは非常にシンプルな設計です。唯一の仕事は、アプリが終了するまで、スレッドセーフなワークキューから作業のブロックを取得して実行することです。こうした作業のブロックの一部は、フレームワークのさまざまな場所で発生します。発生する場所には、ライフサイクル情報に関連するコールバック、入力などのユーザー イベント、他のアプリやプロセスからのイベントなどがあります。これ以外に、アプリが、フレームワークを使用せずに、アプリ自身のブロックを明示的にキューに投入することもできます。

アプリが実行するコードブロックのほとんどは、入力、レイアウトのインフレート、描画などのイベント コールバックに関連しています。イベントがトリガーされると、イベントが発生したスレッドからメインスレッドのメッセージ キューにイベントが投入されます。そして、メインスレッドがイベントを処理できるようになります。

アニメーションや画面更新の間、システムは、毎秒 60 フレームを滑らかにレンダリングするため、約 16 ミリ秒ごとに作業ブロック(画面の描画を担当)を実行します。これを達成するため、システムは、UI / View 階層をメインスレッドで更新する必要があります。ただし、メインスレッドのメッセージング キューにあるタスクが多すぎるか長すぎるために更新処理が追いつかない場合、アプリはこの作業をワーカー スレッドに移動する必要があります。メインスレッドが 16 ミリ秒以内に作業ブロックの実行を終了できない場合、ユーザーがスムーズでない動作や遅れ、入力に対する UI の反応性の悪さを感じる可能性があります。 メインスレッドが約 5 秒間ブロックされると、システムがアプリケーション応答なし(ANR)ダイアログを表示し、ユーザーが直接アプリを閉じることができるようにします。

多数のタスクや長いタスクをメインスレッドから移動して、レンダリングの滑らかさとユーザー入力に対する応答性を損なわないことが、アプリでスレッド化を採用する最大の理由です。

スレッドと UI オブジェクトの参照

設計上、Android View オブジェクトはスレッドセーフではありません。アプリはメインスレッドで UI オブジェクトを作成、使用、破棄すると想定されています。メインスレッド以外のスレッドで UI オブジェクトを変更または参照しようとすると、例外、サイレント エラー、クラッシュ、その他の未定義の不正動作が発生する可能性があります。

参照の問題は、明示的参照と非明示的参照の 2 つのカテゴリに分類されます。

明示的参照

メイン以外のスレッドのタスクは、その多くが UI オブジェクトの更新を最終目標にしています。 しかし、このようなスレッドのいずれかがビュー階層内のオブジェクトにアクセスすると、アプリケーションが不安定になる可能性があります。他のスレッドがオブジェクトを参照しているときに、ワーカー スレッドがそのオブジェクトのプロパティを変更すると、その結果は未定義になります。

たとえば、ワーカー スレッドで UI オブジェクトを直接参照するアプリがあるとします。ワーカー スレッドのオブジェクトには View への参照が含まれる場合があります。ただし、この View は処理が完了する前にビュー階層から削除されます。この 2 つのアクションが同時に発生すると、参照により View オブジェクトがメモリ内に保持され、プロパティが設定されます。しかし、このオブジェクトが表示されることはなく、オブジェクトへの参照がなくなるとアプリにより削除されます。

別の例として、View オブジェクトに、そのオブジェクトを所有するアクティビティへの参照が含まれている場合を考えます。そのアクティビティが破棄されても、それを直接的または間接的に参照するスレッド化された作業ブロックが残っている場合、その作業ブロックの実行が完了するまでガベージ コレクタはそのアクティビティを回収しません。

この状況では、画面の回転などのアクティビティのライフサイクル イベントが発生している間に、スレッド化された作業が実行中であると、問題が発生する可能性があります。 実行中の作業が完了するまで、システムはガベージ コレクションを実行できません。その結果、ガベージ コレクションが行われるまで、メモリ内に 2 つの Activity オブジェクトが存在する可能性があります。

このような状況では、アプリのスレッド化された作業タスクに UI オブジェクトへの明示的参照を含めないようにすることをおすすめします。こうした参照を回避することで、スレッド競合を回避しながら、前述のタイプのメモリリークを回避できます。

すべての場合において、アプリは UI オブジェクトの更新をメインスレッドでのみ行うようにする必要があります。つまり、複数のスレッドがメインスレッドと作業をやり取りできるようにする調停ポリシーを作成する必要があります。メインスレッドは、実際の UI オブジェクトを更新する作業を持つ最上位のアクティビティまたはフラグメントを処理します。

非明示的参照

次のコード スニペットは、スレッド化されたオブジェクトを使ったコード設計によく見られる欠陥の例です。

Kotlin

    class MainActivity : Activity() {
        // ...
        inner class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
            override fun doInBackground(vararg params: Unit): String {...}
            override fun onPostExecute(result: String) {...}
        }
    }

Java

    public class MainActivity extends Activity {
      // ...
      public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
        @Override protected String doInBackground(Void... params) {...}
        @Override protected void onPostExecute(String result) {...}
      }
    }
    

このスニペットの欠陥は、スレッド オブジェクト MyAsyncTask をアクティビティの非静的内部クラスまたは Kotlin の内部クラスとしてコードで宣言していることにあります。この宣言により、包含する Activity インスタンスへの非明示的参照が作成されます。その結果、上記オブジェクトは、スレッド化された作業が完了するまでアクティビティへの参照を含むことになり、参照されるアクティビティの破棄が遅れます。こうして遅延が発生することで、メモリの消費が増えることになります。

この問題の直接的な解決策は、オーバーロードされたクラス インスタンスを静的クラスとして、または独自のファイル内で定義し、非明示的参照をなくすことです。

もう 1 つの解決策は、AsyncTask オブジェクトを静的なネストされたクラスとして宣言するか、Kotlin の inner 修飾子を削除することです。こうすると、静的なネストされたクラスは内部クラスとは異なるため、非明示的参照の問題がなくなります。内部クラスのインスタンスは、インスタンス化のために外部クラスのインスタンスを必要とするため、包含するインスタンスのメソッドとフィールドに直接アクセスします。これに対して、静的なネストされたクラスは、包含するクラスのインスタンスへの参照を必要としないため、外部クラスのメンバーへの参照を含みません。

Kotlin

    class MainActivity : Activity() {
        // ...
        class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
            override fun doInBackground(vararg params: Unit): String {...}
            override fun onPostExecute(result: String) {...}
        }
    }
    

Java

    public class MainActivity extends Activity {
      // ...
      static public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
        @Override protected String doInBackground(Void... params) {...}
        @Override protected void onPostExecute(String result) {...}
      }
    }
    

スレッドとアプリのアクティビティのライフサイクル

アプリのライフサイクルは、アプリケーションでのスレッド処理の動作に影響します。アクティビティが破棄された後、スレッドを残すかどうかを決める必要がある場合があります。また、スレッドの優先順位付けと、アクティビティがフォアグラウンドとバックグラウンドのどちらで実行されているかの関係にも注意する必要があります。

スレッドの存続

スレッドは、それを生成したアクティビティが破棄された後も存続します。スレッドは、アクティビティの作成や破棄に関係なく、実行され、中断もされません。存続されることが望ましい場合もあります。

アクティビティがスレッド化された一連の作業ブロックを生成し、ワーカー スレッドがブロックを実行する前に破棄される場合を考えます。アプリは実行中のブロックをどうすればよいでしょうか。

ブロックが存在しなくなった UI を更新しようとしていた場合、その作業を続行する必要はありません。たとえば、データベースからユーザー情報を読み込んでビューを更新する作業である場合、当該スレッドは不要になります。

逆に、こうした作業パケットが UI には直接関係ないところで役に立つ場合があります。この場合はスレッドを存続させるべきです。たとえば、パケットが画像のダウンロード、ディスクへのキャッシュ、関連する View オブジェクトの更新をしようと待機しているとします。オブジェクトはなくなりますが、破棄したアクティビティにユーザーが戻ったときに、画像のダウンロードとキャッシュは役に立ちます。

すべてのスレッド オブジェクトのライフサイクル応答を手動で管理することは、非常に複雑になる可能性があります。正しく管理しないと、メモリの競合やパフォーマンスの問題が発生する可能性があります。ViewModelLiveData を組み合わせることで、ライフサイクルを気にすることなく、データを読み込んで変更通知を受け取ることができます。ViewModel オブジェクトは、この問題の解決策の 1 つです。ViewModel は構成の変更後も維持されるため、ビューデータを簡単に存続させることができます。ViewModel の詳細については、ViewModel ガイドをご覧ください。また、LiveData の詳細については、LiveData ガイドをご覧ください。アプリケーション アーキテクチャの詳細については、アプリ アーキテクチャ ガイドをご覧ください。

スレッドの優先度

プロセスとアプリケーションのライフサイクルで説明したように、アプリのスレッドに与えられる優先度は、アプリがアプリ ライフサイクルのどこにあるかによって部分的に決まります。アプリケーションでスレッドを作成および管理する際、適切なスレッドに適切なタイミングで適切な優先度が与えられるように、優先度を設定することが重要です。設定が高すぎると、スレッドが UI スレッドと RenderThread に割り込んで、アプリでフレーム落ちが発生する可能性があります。設定が低すぎると、非同期タスク(画像の読み込みなど)が必要以上に遅くなる可能性があります。

スレッドを作成するときには必ず、setThreadPriority() を呼び出してください。システムのスレッド スケジューラは、優先度の高いスレッドを優先し、最終的に作業が完了すればよいスレッドとのバランスをとります。通常、フォアグラウンド グループのスレッドが、デバイスの総実行時間の約 95% を占め、バックグラウンド グループが約 5% を占めます。

また、システムは Process クラスを使用して、各スレッドに個別の優先度値を割り当てます。

デフォルトでは、システムはスレッドの優先度を、生成元のスレッドと同じ優先度とグループ メンバーシップに設定します。ただし、アプリケーションは setThreadPriority() を使用してスレッドの優先度を明示的に調整できます。

Process クラスに定義された一連の定数を使用してスレッドの優先度を設定すると、優先度値の割り当てが簡単になります。たとえば、THREAD_PRIORITY_DEFAULT はスレッドのデフォルト値を表します。それほど緊急ではない処理を実行しているスレッドの優先度には、THREAD_PRIORITY_BACKGROUND を設定してください。

定数 THREAD_PRIORITY_LESS_FAVORABLETHREAD_PRIORITY_MORE_FAVORABLE は、相対的な優先度を設定するための差分として使用できます。スレッドの優先度のリストについては、Process クラスの THREAD_PRIORITY 定数をご覧ください。

スレッド管理の詳細については、Thread クラスと Process クラスに関するリファレンス ドキュメントをご覧ください。

スレッド化用のヘルパークラス

このフレームワークには、Thread クラス、Runnable クラス、Executors クラスなど、スレッド化を容易にする同じ Java クラスとプリミティブが用意されています。Android 用にスレッド化されたアプリケーションを開発する際の認知的負荷を軽減するために、フレームワークには、AsyncTaskLoaderAsyncTask など、開発の助けになるヘルパーが用意されています。各ヘルパークラスにはパフォーマンスに関して微妙な違いがあります。それぞれがスレッド化に関する別々の問題に対応しています。間違った状況に間違ったクラスを使用すると、パフォーマンスの問題が発生する可能性があります。

AsyncTask クラス

AsyncTask クラスはシンプルなプリミティブで、アプリでメインスレッドからワーカー スレッドに作業をすばやく移動するのに便利です。たとえば、入力イベントにより、読み込まれたビットマップで UI を更新する必要が生じる場合があります。AsyncTask オブジェクトは、ビットマップの読み込みとデコードを代替スレッドにオフロードできます。処理が完了すると、AsyncTask オブジェクトはメインスレッドで作業を受け取り、UI を更新できます。

AsyncTask を使用する場合は、パフォーマンスに関して考慮すべき重要な事項がいくつかあります。まず、デフォルトでは、アプリが作成したすべての AsyncTask オブジェクトは 1 つのスレッドに投入されます。したがって、それらは順番に実行され、メインスレッドと同様、特に長い作業パケットにキューがブロックされる可能性があります。このため、AsyncTask を使用して処理するのは、5 ミリ秒未満の作業項目のみにすることをおすすめします。

AsyncTask オブジェクトは、非明示的参照の問題の最もよくある原因でもあります。また、AsyncTask オブジェクトには明示的参照に関連するリスクもあります。ただし場合によっては、こちらのほうがむしろ回避が容易です。たとえば、AsyncTask がメインスレッドでコールバックを実行する場合、AsyncTask は UI オブジェクトを適切に更新するために UI オブジェクトへの参照を必要とする場合があります。この状況では、WeakReference を使用して必要な UI オブジェクトへの参照を保存できます。また、AsyncTask がメインスレッドで動作すると、オブジェクトにアクセスできるようになります。なお、WeakReference をオブジェクトに保持しても、オブジェクトがスレッドセーフになるわけではありません。WeakReference では、明示的参照とガベージ コレクションに関する問題に対処する方法のみが提供されます。

HandlerThread クラス

AsyncTask は便利ですが、スレッド化の問題に対して常に正しい解決策になるとは限りません。代わりに、動作時間の長いスレッドで作業ブロックを実行する既存の方法と、そのワークフローを手動で管理する方法が必要になる場合があります。

Camera オブジェクトからプレビュー フレームを取得する際に直面する一般的な問題について考えてみましょう。Camera プレビュー フレームに登録すると、onPreviewFrame() コールバックでフレームを受け取ります。このコールバックは、呼び出し元のイベント スレッドで呼び出されます。このコールバックが UI スレッドで呼び出された場合、巨大なピクセル配列を処理するタスクは、レンダリングとイベント処理の作業を妨げることになります。同じ問題が AsyncTask にも発生します。この場合もジョブは順番に実行され、ブロックされやすくなります。

これは、ハンドラ スレッドが適切である状況です。ハンドラ スレッドは、実質的に、キューから作業を取得して処理する長時間動作するスレッドです。この例では、アプリは Camera.open() コマンドをハンドラ スレッドの作業ブロックに委任し、関連付けられた onPreviewFrame() コールバックは、UI スレッドまたは AsyncTask スレッドではなく、ハンドラ スレッドで呼び出されます。そのため、ピクセルに長時間かかる処理を行う場合、これが良い解決方法になるかもしれません。

アプリが HandlerThread を使用してスレッドを作成するときは、スレッドの優先度を作業の種類に基づいて設定することを忘れないでください。また、CPU は少数のスレッドしか並列に処理できないことに注意してください。優先度を設定することにより、システムが、すべてのスレッドが競い合う中でこの作業を適切にスケジューリングできます。

ThreadPoolExecutor クラス

高度に並列化された分散タスクに還元できる処理には、いくつかの種類があります。8 メガピクセルの画像の 8 × 8 ブロックごとにフィルタを計算するのは、その一例です。これにより生成される膨大な量の作業パケットに、AsyncTask クラスと HandlerThread クラスは、適切ではありませんAsyncTask では、シングル スレッドの性質上、スレッドプールされたすべての作業が線形システムに変わります。一方で、HandlerThread クラスを使用する場合、スレッド グループ内の負荷分散をプログラマーが手動で管理する必要があります。

ThreadPoolExecutor は、この処理を簡単にするヘルパークラスです。このクラスは、スレッドのグループの作成を管理して、優先度を設定し、スレッド間での作業の分散方法を管理します。このクラスは、ワークロードの増減に合わせてスレッドを起動または破棄します。

また、このクラスを使用して、アプリが生成するスレッドの数を最適化できます。アプリで ThreadPoolExecutor オブジェクトを作成するときに、スレッド数の下しきい値と上しきい値を設定します。ThreadPoolExecutor は、与えられたワークロードが増加すると、初期設定されたスレッド数の下限と上限、および実行しようとしている保留中の作業の量を考慮します。ThreadPoolExecutor は、こうした要因に基づき、同時に存在するスレッド数を決定します。

作成するスレッドの数

ソフトウェア レベルでは、コードで何百ものスレッドを作成できますが、そうすることで、パフォーマンスの問題が発生する可能性があります。アプリは、限られた CPU リソースをバックグラウンド サービス、レンダラ、オーディオ エンジン、ネットワーク機能などと共有します。実際には、CPU は少数のスレッドしか並列に処理できません。上記のすべては優先度とスケジューリングの問題に突き当たります。そのため、ワークロードに必要な数のスレッドのみを作成することが重要です。

実際には、これには多くの変数が関与しますが、値を選択し(最初は 4 など)、Systrace でテストすることは、他の方法と同様に堅実な方法です。試行錯誤により、問題が発生せずに使用できる最小のスレッド数を見つけることができます。

スレッド数を決める際にもう 1 つ考えないといけないのは、スレッドがメモリを消費することです。各スレッドには最低 64 k のメモリが必要です。そのため、デバイスにインストールされた多数のアプリで、メモリ使用量が急激に増加します。特にコールスタックが大幅に増える場合に顕著となります。

多くのシステム プロセスとサードパーティのライブラリは、多くの場合、それぞれが各自のスレッドプールを起動します。アプリが既存のスレッドプールを再利用できる場合、メモリや処理リソースの競合を減らしてパフォーマンスを向上させることができます。