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

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 インスタンスへの非明示的参照が作成されます。その結果、上記オブジェクトは、スレッド化された作業が完了するまでアクティビティへの参照を含むことになり、参照されるアクティビティの破棄が遅れます。こうして遅延が発生することで、メモリの消費が増えることになります。

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

もう一つの解決策は、適切な Activity ライフサイクル コールバック(onDestroy など)でバックグラウンド タスクを常にキャンセルし、クリーンアップすることです。ただし、この方法は煩雑なため、間違いを起こしやすくなります。原則として、複雑な UI 以外のロジックはアクティビティに直接入れないでください。また、AsyncTask は非推奨になっているため、新しいコードでの使用はおすすめしません。使用可能な同時実行プリミティブの詳細については、Android でのスレッド化をご覧ください。

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

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

スレッドの存続

スレッドは、それを生成したアクティビティが破棄された後も存続します。スレッドは、アクティビティの作成や破棄に関係なく、実行され、中断もされませんが、アクティブなアプリケーション コンポーネントがなくなると、アプリのプロセスとともに終了します。存続されることが望ましい場合もあります。

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

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

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

すべてのスレッド オブジェクトのライフサイクル応答を手動で管理することは、非常に複雑になる可能性があります。正しく管理しないと、メモリの競合やパフォーマンスの問題が発生する可能性があります。ViewModelLiveData を組み合わせることで、ライフサイクルを気にすることなく、データを読み込んで変更通知を受け取ることができます。ViewModel オブジェクトは、この問題の解決策の一つです。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 クラスに関するリファレンス ドキュメントをご覧ください。

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

Kotlin をメインの言語として使用するデベロッパーの場合は、コルーチンを使用することをおすすめします。コルーチンには、コールバックを使用しない非同期コードの記述や、スコープ設定、キャンセル、エラー処理のための構造化された同時実行など、多くの利点があります。

このフレームワークには、Thread クラス、Runnable クラス、Executors クラスなど、スレッド化を容易にする同じ Java クラスとプリミティブに加えて、HandlerThread などのクラスも用意されています。詳しくは、Android でのスレッド化をご覧ください。

HandlerThread クラス

ハンドラ スレッドは、実質的に、キューから作業を取得して処理する長時間動作するスレッドです。

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

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

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

ThreadPoolExecutor クラス

高度に並列化された分散タスクに還元できる処理には、いくつかの種類があります。8 メガピクセルの画像の 8 × 8 ブロックごとにフィルタを計算するのは、その一例です。これにより生成される膨大な量の作業パケットに HandlerThread を使用するのは、適切ではありません。

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

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

作成するスレッドの数

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

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

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

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