Androidのバックグラウンドを使いこなす Thread, Looper, Handler

Android UIは基本的にシングルスレッドで動作します。そのため、この影響を考慮して開発しなければアプリケーションの性能が低下することがあります。したがって、メインスレッドで時間のかかる処理をするのを避けるために、他のスレッドを使用しなければなりません。他のスレッドからUIスレッドにアクセスするためのAndroidが提供するスレッド間通信の方法を紹介します。


イントロダクション

Androidのアプリケーションを実行すると、システムはメインアクティビティをメモリに読み込んでプロセスを作ります。この時メインスレッドが自動的に生成されます。メインスレッドはAndroidの主要コンポーネントを実行するところであり、UIを描いたり更新する仕事を担当できる唯一のスレッドであるためUIスレッドとも呼ばれます。

Androidの画面を構成するビューやビューグループに対する操作を一つのスレッドのみが担当するモデルをシングルスレッドモデルと呼びます。シングルスレッドモデルの原則は第一にメインスレッド(UIスレッド)をブロックしないこと、第二にAndroid UIフレームワークはUIスレッドからのみアクセスすること、の二種類です。このようなシングルスレッドモデルの影響を考慮しないとアプリケーションの性能が低下することがあります。長い時間がかかる作業をメインスレッドで担当するとアプリケーションの反応が悪くなります。また最終的にはユーザの不便を防止しようとシステムがアプリケーションをANR(Appication Not Responding)状態にすることもあります。したがって、時間がかかる処理をするコードはバックグラウンドスレッドを使用してメインスレッドから分離しなければならないため、自然とメインスレッドと他のスレッドが通信する方法が必要になります。

他のスレッドからメインスレッドと通信するために、LooperとHandlerを使用することができます。AndroidはJavaのThreadをより簡単に扱えるようにラッピングしたHandlerThreadや、ThreadまたはMessage Loopなどの動作原理を熟知していなくても使えるAsyncTaskなどのクラスを提供します。本稿では先にThread、Looper、Handlerの概念を説明し、その後でHandlerThreadとAsyncTaskについて見るようにします。

LooperとHandlerの使用目的

なぜAndroidはメインスレッドでのみUIの操作が可能になるように制限されているのでしょうか。メインスレッドでないスレッドが並列で実行されているときに、メインスレッドと他のスレッドの二つ以上のスレッドが同時に同じテキストビューにsetText()を試みる場合を考えてみるとわかりやすいです。

二つのうちどちらのスレッドのsetText()が適用されるかは予測できず、ユーザからは二つのうち一つの値のみを見ることができ、他のスレッドの結果は捨てられます。このように二つ以上のスレッドを使用する際の同期問題を解消するためにLooperとHandlerを使用することになります。

LooperとHandlerの仕組み

まずスレッドとLooper、Handlerがどのように動作するのか見てみましょう。メインスレッドは内部的にLooperを持っており、その中にはMessage Queueが含まれます。Message Queueはスレッドが他のスレッドやあるいは自分自身から受け取ったMessageを基本的に先入先出法で管理するQueueです。LooperはMessage QueueでMessageやRunnableオブジェクトを順に取り出してHandlerが処理するように伝えます。HandlerはLooperから受けたMessageを実行、処理したり、他のスレッドからメッセージを受けてMessage Queueに入れる役割を行うスレッド間の通信装置です。

ではHandlerとLooper、Message Queueについてもう少し詳しく見てみましょう。

Handler

HandlerはスレッドのMessage Queueと連携してMessageやRunnableオブジェクトを受けたり、処理してスレッド間通信をできるようにします。Handlerオブジェクトは一つのスレッドと該当スレッドのMessage Queueに依存しています。新たにHandlerオブジェクトを作った場合、これを作ったスレッドと当該スレッドのMessage Queueにバインドされます。あるスレッドから特定のスレッドにメッセージを届けるには特定のスレッドに属したHandlerのpostやsendMessageなどのメソッドを呼び出します。 さきほどMessage Queueは受け取ったMessageを先入先出法で管理すると説明しましたが、送信時に別のメソッドを使用してQueueの一番上に送ったり、望むだけMessageやRunnableオブジェクトの送信を遅延させることもできます。よく使われるHandlerのメソッドを下表にまとめました。

戻り値型 メソッド名 引数 説明
void handleMessage Message msg LooperがMessage Queueで取り出したMessageやRunnableオブジェクトを処理(継承時実装必須)
boolean post Runnable r Message QueueにRunnableを渡す
boolean sendMessage Message msg Message QueueにMessageを渡す
boolean postAtFrontOfQueue Runnable r Message Queueの先頭にRunnableを渡す
boolean sendMessageAtFrontOfQueue Message msg Message Queueの先頭にMessageを渡す
boolean postDelayed Runnable r, long delayMillis delayMillisミリ秒後 に処理されるようにRunnableをMessage Queueに渡す
boolean sendMessageDelayed Message msg, long delayMillis delayMillisミリ秒後 に処理されるようにMessageをMessage Queueに渡す

受け取ったメッセージをどの様に処理するかはhandleMessage()メソッドを実装して定めます。sendMessage()やpost()で特定のHandlerにメッセージを渡すことができ、再帰的な呼び出しもできるので遅延を利用したタイマーやスケジューリングの役割もできて便利です。

LooperMessage Queue

Looperは無限にループしながら自分が属したスレッドのMessage Queueに入ってきたMessageやRunnableオブジェクトを順に取り出してこれを処理するHandlerに伝える役割をします。メインスレッドはLooperが基本的に生成されていますが、新たに生成したスレッドは基本的にLooperを持っていないため、単にrunメソッドだけ実行した後、終了するためにメッセージを受け取ることができません。したがって基本的にスレッドでメッセージを受けるためにはprepare()メソッドでLooperを生成して、loop()メソッドでLooperを無限ループさせます。このLooperがループしながらMessage QueueにたまったMessageやRunnableオブジェクトを取り出してHandlerに渡します。動作中のLooperはquit()やquitSafely()メソッドで停止することができます。quit()メソッドが呼び出されるとLooperは直ちに終了され、quitSafely()メソッドではMessage Queueにたまっているメッセージを処理したあとに終了します。

MessageRunnable

Messageとはスレッド間通信する内容を盛り込むオブジェクトであり、Queueに入るタスクの単位でHandlerを使って送ることができます。Messageが必要なときに常に新しいMessageオブジェクトを生成するとパフォーマンスが悪いため、Androidがシステムに作っておいたMessage Pool内のオブジェクトを再利用します。obtain()メソッドは空のMessageオブジェクトを、obtain(Handler h、int what…)は対象とするHandlerと他の引数を盛り込んだMessageオブジェクトを返却します。 Runnableの説明の前に、まずスレッドを作る二つの方法を知る必要があります。新しいスレッドはThreadクラスを継承するクラスを作ってrun()メソッドをオーバーライドするか、Runnableインタフェースを実装したオブジェクトを生成してThread(Runnable runnable)コンストラクタに渡すかのどちらかの方法で生成することになります。後者で使用するものはRunnableからスレッドのrun()メソッドを分離したものです。したがってRunnableインタフェースはrun()抽象メソッドを持っているので継承したクラスはrun()メソッドを必ず実装しなければなりません。 前述したようにMessageがintやObjectのようなスレッド間通信する内容とすると、Runnableは実行するrun()メソッドとその内部で実行されるコードという違いがあります。

HandlerThread

Looperで言及したようにAndroidのスレッドはJavaのスレッドを使用するためAndroidで導入したLooperを持っていないという不便さがあります。このような不便さを改善するために、生成時にLooperを自動的に持つクラスを提供しています。これがまさにHandlerThreadです。 HandlerThreadは一般的なスレッドを拡張したクラスで、内部に繰り返しループするLooperを持っています。自動的にLooper内部のMessage Queueも生成されるためこれを利用してスレッドにMessageやRunnableを渡すことができます。

AsyncTask

AsyncTaskはスレッドやメッセージループなどの仕組みを知らなく���も、一つのクラスでUIとバックグラウンド処理の両方を簡単に扱えるAndroidが提供するクラスです。うまくカプセル化されているため、コードの可読性が増す長所があり、タスクのスケジュールを管理できるコールバックメソッドも提供されています。必要な時に簡単にUIの更新もできて、キャンセル処理も簡単です。したがって、リストに表示するデータダウンロードなどUIと関連したり独立したタスクを実行する場合はAsyncTaskで簡単に実装することができます。

図: AsyncTaskの構造

しかし、AsyncTaskを使用してスケジューリングするできるタスク数には制限があり、数秒程度の短いタスクでのみ理想的に動作するという限界があります。また、Androidのバージョン毎に並列処理の挙動が異なるので、ハニカム以降のバージョンでマルチスレッドで並列処理を行う場合は、AsyncTaskを実行する際にAsyncTask.THREAD_POOL_EXECUTORスケジューラを指定する必要があります。 一方、先に見たHandlerとLooperを使用する場合は仕組みを動作原理を熟知していなければならず、より低レイヤーで実装しなければならないためコードが複雑になって可読性が悪くなるという短所がありますが、その分制限を気にすること無く自由に開発できます。また、UIスレッドでのみで動作する必要がなく、より自由に開発したい場合はHandlerやHandlerThreadの使用を検討してみてください。

参考サイト

タイトルにリンクされたAndroid開発者のサイトを熟読すると、該当クラスやインターフェイスについてより理解が深まります。それ以外でも下記のサイトがとても参考になりました。


Eunjoo Im

Eunjoo Im

Eunjoo Im is a mobile developer who works in Realm who lives in South Korea. Outside of work, she enjoys building iOS apps, reading, writing, games, photography, and learning new things.