UIスレッドとの連携
前節の内容で、非同期処理の基本形がほぼできあがり、これで問題なく動作するように思えます。しかし、ここにはAndroid特有の問題が含まれています。そこを解決していきましょう。
バックグランド処理の結果をUIスレッドに戻す
前節で紹介した非同期処理のコードパターンを図2のような図に起こしてみると、図3のようになります。
例えば、ネット上から画像ファイルをダウンロードしてストレージに格納する処理といったような、バックグラウンド処理の内容が処理終了後もそのままで問題ないならば、つまり、処理がバックグラウンドのみで完結するならば、リスト1のコードパターンで問題ありません。
一方、本稿が題材にしている、お天気情報をWeb APIから取得して、その内容を画面に表示させる処理といったように、バックグラウンド処理の終了後、その結果をもとにUIスレッドで何か処理を行わないといけない場合は、もう一工夫必要です(図4)。
非同期処理をUIスレッドに戻すHandler
バックグラウンド処理の終了後に、その結果をもとにUIスレッドで処理を行おうとすると、実は、ピュアJavaだけでは難しいです。それを想定してか、Androidでは、独自のSDKとして、その仕組みを実現するクラスとしてHandlerとLooperというのが用意されています。これらを利用します。
Handleは、スレッド間の通信を行ってくれるオブジェクトです。元となるスレッドで事前にHandlerオブジェクトを用意しておき、これを、BackgroundTaskに渡します。BackgroundTaskのrun()メソッド内で、このオブジェクトのpost()メソッドを実行することで、その時点でHandlerオブジェクトを生成した元スレッドで処理を行ってくれます。その処理内容を、これまたRunnableインスタンスとしてpost()の引数に渡します。
ここまでの内容をコードパターンにすると、リスト2のようになります。
private class BackgroundTask implements Runnable { private final Handler _handler; // (1) public BackgroundTask(Handler handler) { // (2) _handler = handler; // (2) } @Override public void run() { Log.i("Async-BackgroundTask", "ここに非同期処理を記述する"); PostExecutor postExecutor = new PostExecutor(); _handler.post(postExecutor); // (3) } } private class PostExecutor implements Runnable { // (4) @Override public void run() { Log.i("Async-PostExecutor", "ここにUIスレッドで行いたい処理を記述する"); // (5) } }
先述の通り、Handlerオブジェクトは、元となるスレッドで用意する必要があります。ということは、バックグラウンドスレッドで実行するBackgroundTaskでは、これを受け取り、内部で保持する必要があります。それを実現するために、コンストラクタで受け取るようにしています(リスト2の(2))。コンストラクタで受け取ったHandlerオブジェクトを格納するためのフィールドがリスト2の(1)です。ただし、このような複数スレッドを跨いで利用される変数は、各スレッドから書き換えが起きれば大変です。そこで書き換えができないようにfinalキーワードを付与して、複数のスレッドで問題なく動作するようにしておきます。このことを、スレッドセーフといいます。
そのようにして利用できるようにしたHandlerオブジェクトのpost()メソッドを実行しているのが(3)です。非同期処理を実行した後に実行しています。その引数として渡しているのが、(4)のPostExecutorをnewしたオブジェクトです。この(4)のrun()メソッドに、バックグラウンド処理終了後にUIスレッドで実行したい処理を記述することで、非同期で処理を戻すことが可能となります。リスト2では、(5)が該当し、ここでも単純にログへの書き出し処理のみを行っています。
確実にUIスレッドに処理を戻すにはLooperが必要
ところで、リスト2では、そもそもHandlerオブジェクトの取得方法が記述されていません。実は、Handlerオブジェクトは、あくまでスレッド間通信を行うためのオブジェクトであり、バックグラウンド処理の後に確実にUIスレッドを実行させるためには、Looperオブジェクトに登場してもらう必要があります。これも、Android SDKで用意されたクラスです。Handlerオブジェクトを生成する際に、LooperのgetMainLooper()メソッドの戻り値を渡すことで、確実にgetMainLooper()を実行したスレッド、つまり、UIスレッドに処理を戻すことができます。
ここまでの内容をまとめたコードパターンは、リスト3のようになります。
@Override protected void onCreate(Bundle savedInstanceState) { 〜省略〜 Looper mainLooper = Looper.getMainLooper(); // (1) Handler handler = HandlerCompat.createAsync(mainLooper); // (2) BackgroundTask backgroundTask = new BackgroundTask(handler); // (3) ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.submit(backgroundTask); } private class BackgroundTask implements Runnable { private final Handler _handler; public BackgroundTask(Handler handler) { _handler = handler; } @Override public void run() { Log.i("Async-BackgroundTask", "ここに非同期処理を記述する"); PostExecutor postExecutor = new PostExecutor(); _handler.post(postExecutor); } } private class PostExecutor implements Runnable { @Override public void run() { Log.i("Async-PostExecutor", "ここにUIスレッドで行いたい処理を記述する"); } }
リスト3の(1)で、LooperのgetMainLooper()メソッドを実行し、その戻り値をmainLooperとしています。この実行処理をUIスレッドで行うことで、mainLooperが確実にUIスレッドを表すことになります。これを引数として、リスト(2)のようにHandlerCompatのcreateAsync()メソッドを利用してHandlerオブジェクトを生成します。この段階で、このHandlerオブジェクトであるhandlerが、戻り先としてUIスレッドを保証してくれます。これを、バックグラウンド処理を行うBackgroundTaskに渡します。それが、リスト3の(3)です。
@UIThreadと@WorkerThread
リスト3のコードパターンで、バックグラウンド処理、および、その後のUIスレッドでの処理パターンに対応したコードが記述できました。これらを受けて、もう一段階進めて完成に近づけます。
Androidでは、ここまで説明したように、UIスレッドとバックグラウンド処理であるワーカースレッドとのやり取りを想定したSDK設計になっています。そのため、それぞれの処理を記述したソースコードには、それぞれに対応したアノテーションを付与できるようになっています。
リスト3では、(1)のmainLooperの取得からsubmit()の実行までがUIスレッドでの処理となります。また、PostExecutorのrun()メソッドもUIスレッドでの処理となります。一方、BackgroundTaskのrun()メソッドはワーカースレッドです。これらをアノテーションとして明記すると、リスト4のようになります。
@Override protected void onCreate(Bundle savedInstanceState) { 〜省略〜 asyncExecute(); } @UiThread // (1) public void asyncExecute() { // (2) Looper mainLooper = Looper.getMainLooper(); Handler handler = HandlerCompat.createAsync(mainLooper); BackgroundTask backgroundTask = new BackgroundTask(handler); ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.submit(backgroundTask); } private class BackgroundTask implements Runnable { private final Handler _handler; public BackgroundTask(Handler handler) { _handler = handler; } @WorkerThread // (3) @Override public void run() { Log.i("Async-BackgroundTask", "ここに非同期処理を記述する"); PostExecutor postExecutor = new PostExecutor(); _handler.post(postExecutor); } } private class PostExecutor implements Runnable { @UiThread // (4) @Override public void run() { Log.i("Async-PostExecutor", "ここにUIスレッドで行いたい処理を記述する"); } }
先述のように、リスト3のmainLooperの取得からsubmit()の実行までがひとつの纏まった処理ですので、これをasyncExecute()というメソッドにまとめています(リスト4の(2))。もちろん、メソッド名は何でもかまいません。大切なのは、これらをまとめてひとつのメソッドにして、それに対して(1)のように「@UiThread」というアノテーションを付与しています。これによって、このメソッドがUIスレッドで実行されることがコンパイラによって保証されます。もし不都合があるならば、コンパイルエラーの形で教えてくれます。
同様なのが、リスト4の(4)です。PostExecutorのrun()メソッド内の処理は、バックグラウンド処理後にUIスレッドで実行される処理ですので、@UiThreadを付与しています。
一方、BackgroundTaskクラスのrun()メソッドは、バックグラウンド処理であるワーカースレッドで実行されるため、リスト4の(3)のように@WorkerThreadアノテーションを付与しています。
ここで注意してほしいのは、アノテーションを付与するのは、あくまでメソッドに対してである、ということです。例えば、BackgroundTaskクラスに対して@WorkerThreadアノテーションを付与することも可能ですが、その瞬間にコンパイルエラーとなります。というのは、BackgroundTaskクラスのコンストラクターの処理はUIスレッドで行われるからです。そのため、確実に、UIスレッド行われるメソッドに対して@UiThreadアノテーションを、バックグラウンド処理が行われるメソッドに対して@WorkerThreadアノテーションを付与するようにしましょう。
まとめ
ライブドアのお天気情報サービスが終了となり、さらには、非同期処理で基本として利用してきたAsyncTaskクラスが非推奨となり、それらを補うために現状に合った処理方法を紹介する本稿はいかがでしたか?
今回はJavaによるAndroidの非同期処理を紹介しましたが、あくまでコードパターンしか紹介していません。実際に、新しいWeb APIサービスからお天気情報を取得して表示する内容などは、何も紹介していません。
次回、今回紹介したコードパターンを、実際のAndroidアプリとして組み込み、さらには、現在利用可能なOpen WeatherのWeb APIサービスを利用して、実際にHTTPアクセスを行い、お天気情報を表示する方法を紹介します。