Help us understand the problem. What is going on with this article?

Androidで非同期処理

More than 1 year has passed since last update.

はじめに

Androidで通信処理(APIアクセスなど)を行うには、別スレッドを用いて非同期で行う必要があります。そうしないと例外が吐かれます。ここでは、こうした非同期処理を実装する際にハマったことなどから、色々まとめたいと思います。
さっさとベストな方法を教えろ!という方は,一番下まで飛ばせばよろしいかと思います.

ここで使うクラス

  • android.os.AsyncTask
  • java.util.concurrent.CountDownLatch

大事なのはこの2つです。

AsyncTaskとは

Androidで非同期処理させる際によく用いられます。UIスレッドからの分離など、面倒くさいことは勝手にやってくれるので、楽に処理を書けます。なお、以下の点に従う必要があります。

  • ジェネリックなクラスなので、Params,Progless,Resultは必要な型に置き換える。
  • 実行時はexecute()メソッドを呼び出す。
  • 1つのインスタンスからは、1回しかexecute()メソッドを呼び出せない。
  • 以下の4つのprotectedなメソッドを手動で呼び出してはいけない。

よく使うのは以下の4つのメソッドです。それぞれの役割を簡単に書いておきます。

MyAsyncTask.java
import android.os.AsyncTask;

public class MyAsyncTask extends AsyncTask<Params, Progress, Result> {

    @Override
    protected void onPreExecute() {
        //バックグラウンド処理開始前にUIスレッドで実行される。
        //ダイアログの生成などを行う。
    }

    @Override
    protected Result doInBackground(Params... params) {
        //バックグラウンドで処理させる内容をここで記述。
        //AsyncTaskを使うにあたって、このメソッドの中身は必ず記述しなければいけない。
    }

    @Override
    protected void onProgressUpdate(Progress... values) {
        //doInBackgroundの実行中にUIスレッドで実行される。
        //引数のvaluesを使ってプログレスバーの更新などをする際は、ここに記述する。
    }

    @Override
    protected void onPostExecute(Result result) {
        //doInBackgroundが終了するとUIスレッドで実行される。
        //ダイアログの消去などを行う。
        //doInBackgroundの結果を画面表示に反映させる処理もここに記述。
    }
}

処理を実行するには、実行させたい場所で以下のように記述します。

MyParallelActivity.java
~省略~
    //タスクごとにインスタンスを生成
    MyAsyncTask task = new MyAsyncTask();

    //処理を実行
    //param1,param2はそれぞれdoInBackgroundの引数
    task.execute(param1,param2);
~省略~

CountDownLatchとは

非同期処理を同期させる際などに使用します。指定した数だけ非同期処理が終了すると処理を再開させるスイッチみたいなものです。

MyTaskThread.java
public class MyTaskThread extends Thread {
    private CountDownLatch latch;

    public MyTaskThread(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        //処理
        hoge();

        //MyTaskThreadの処理が完了したことを知らせる
        latch.countDown();
    }
}
MyLatchClass.java
~省略~
    //2カウントのCountDownLatchを生成
    //同期させるタスクの数をコンストラクタで渡す(今回はtask1とtask2の2つを同期させるので2を渡す)
    CountDownLatch latch = new CountDownLatch(2);
    //タスクごとにインスタンスを生成
    //必ず各タスクにlatchを渡す
    //今回はコンストラクタでlatchを渡す
    MyTaskThread task1 = new MyTaskThread(latch);
    MyTaskThread task2 = new MyTaskThread(latch);

    //処理を実行
    task1.start();
    task2.start();

    try {
    //task1とtask2の両方の処理が完了するまで待機
        latch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
~省略~

たったこれだけで利用できます。簡単ですね。

直列処理の基本

以下のように記述します。

MySerialActivity.java
~省略~
    //タスクごとにインスタンスを生成
    MyAsyncTask task1 = new MyAsyncTask();
    MyAsyncTask task2 = new MyAsyncTask();

    //task1の処理完了後にtask2が実行される
    //つまり直列処理
    //param1...param5はそれぞれdoInBackgroundの引数
    task1.execute(param1,param2);
    task2.execute(param3,param4,param5);

    //以下の記述でも直列処理となる
    task1.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,param1,param2);
    task2.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,param3,param4,param5);
~省略~

必ずtask1とtask2が実行されるのであれば、paramで場合分けするのではなく、doInBackgroundの中でこの2つが順番に実行されるように記述するほうが楽かもしれません。

並列処理の基本

基本的には直列処理の場合と同じです。

MyParallelActivity.java
~省略~
    //タスクごとにインスタンスを生成
    MyAsyncTask task1 = new MyAsyncTask();
    MyAsyncTask task2 = new MyAsyncTask();

    //以下の記述で並列処理
    task1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,param1,param2);
    task2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,param3,param4,param5);
~省略~

簡単ですね。ただし、executeOnExecutor(...)は必ずUIスレッドで呼び出さなければいけません。例えば、AsyncTaskを入れ子にしたりすると怒られます。本来はexecute()も同様に怒られるはずなのですが、筆者は怒られませんでした。

AsyncTaskの処理結果をUIに反映させたい場合

さて、ついに本丸に来ました。例えば、複数のAPIにアクセスして得たデータを一気に表示させる場合はどうしましょう。1つ1つのAPIに順番にアクセスしてその都度表示させますか?しかし、これでは時間がかかってしまいます。こういう場合は、全APIへ同時にアクセスして、全てのデータが揃った段階で表示させたり処理させたりしたいですよね。
しかし、MyAsyncTaskのインスタンスが複数ある関係で、onPostExecuteが呼び出される段階ではデータが不完全の可能性が高いですから、onPostExecuteで表示処理をさせるという手は使えません。実現するにはどうしたらいいでしょうか。

失敗例1

MyParallelActivity.java
~省略~
    //データを格納するクラスMyDataのインスタンスを生成
    MyData data = new MyData();

    //タスクごとにインスタンスを生成
    //今回はコンストラクタでデータを渡すこととする
    MyAsyncTask task1 = new MyAsyncTask(data);
    MyAsyncTask task2 = new MyAsyncTask(data);

    //以下の記述で並列処理
    task1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,param1,param2);
    task2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,param3,param4,param5);

    //dataの中身を表示させる
    dispaly(data);
~省略~

これではNullPointerExceptionを吐かれてしまいます。dataへの格納処理は別スレッドで行っているので当然ですね。dataへの格納が完了するのを待つ必要があります。

失敗例2

ではこれではどうでしょうか。ここではバックグラウンド処理中にProgressDialogを表示させています。

MyAsyncTask.java
public class MyAsyncTask extends AsyncTask<Params, Progress, Result> {
    private MyData data;
    private CountDownLatch latch;
    private Activity activity;
    private ProgressDialog myProgressDialog;

    public MyAsyncTask(MyData data, CountDownLatch latch, Activity activity) {
        this.data = data;
        this.latch = latch;
        this.activity = activity;
    }

    @Override
    protected void onPreExecute() {
        //ダイアログを表示させる
        myProgressDialog = new ProgressDialog(activity);
        myProgressDialog.setMessage("メッセージ");
        myProgressDialog.show();
    }

    @Override
    protected Result doInBackground(Params... params) {
        //APIアクセスなどのバックグラウンド処理
        //処理完了を知らせる
        latch.countDown();
        return result;
    }

    @Override
    protected void onPostExecute(Void v) {
        //処理が完了したのでダイアログを消す
        if (myProgressDialog != null && myProgressDialog.isShowing()) {
            myProgressDialog.dismiss();
        }
    }
}
MyParallelActivity.java
~省略~
    //データを格納するクラスMyDataのインスタンスを生成
    MyData data = new MyData();

    CountDownLatch latch = new CountDownLatch(2);

    //タスクごとにインスタンスを生成
    //今回はコンストラクタでデータを渡すこととする
    MyAsyncTask task1 = new MyAsyncTask(data, latch);
    MyAsyncTask task2 = new MyAsyncTask(data, latch);

    //以下の記述で並列処理
    task1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, param1, param2);
    task2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, param3, param4, param5);

    try {
        //並列処理終了まで待機
        latch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    //dataの中身を表示させる
    dispaly(data);
~省略~

dataの中身の表示には成功しますが、ProgressDialogが表示されません。これは、latch.await()がUIスレッドで実行されてしまっているからです。この間、アプリは固まってしまってユーザーの操作を受け付けなくなってしまいます。これでは糞アプリと言われてしまいます。

成功例

こうすれば解決します。

MyAsyncTask.java
public class MyAsyncTask extends AsyncTask<Params, Progress, Result> {
    private MyData data;
    private Activity activity;
    private ProgressDialog myProgressDialog;

    public MyAsyncTask(MyData data, CountDownLatch latch, Activity activity) {
        this.data = data;
        this.latch = latch;
        this.activity = activity;
    }

    @Override
    protected void onPreExecute() {
        //ダイアログを表示させるなどのUIの準備
        myProgressDialog = new ProgressDialog(activity);
        myProgressDialog.setMessage("メッセージ");
        myProgressDialog.show();
    }

    @Override
    protected Result doInBackground(Params... params) {
        CountDownLatch latch = new CountDownLatch(2);
        //処理させたいタスクのインスタンスを生成
        //今回はコンストラクタでデータを渡すこととする
        MyTaskThread task1 = new MyTaskThread(data,latch);
        MyTaskThread task2 = new MyTaskThread(data,latch);
        //ここから並列処理
        task1.start();
        task2.start();

        try {
            //並列処理終了まで待機
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return result;
    }

    @Override
    protected void onPostExecute(Result result) {
        //処理が完了したのでダイアログを消す
        if (myProgressDialog != null && myProgressDialog.isShowing()) {
            myProgressDialog.dismiss();
        }

        display(data);
    }
}
MyTaskThread.java
public class MyTaskThread extends Thread {
    private MyData data;
    private CountDownLatch latch;

    public MyTaskThread(MyData data,CountDownLatch latch) {
        this.data = data;
        this.latch = latch;
    }

    @Override
    public void run() {
        //処理
        hoge();
        //MyThread1の処理が完了したことを知らせる
        latch.countDown();
    }
}
MyParallelActivity.java
~省略~
    //タスクごとにインスタンスを生成
    MyAsyncTask task = new MyAsyncTask();

    //処理を実行
    //param1,param2はそれぞれdoInBackgroundの引数
    task.execute(param1,param2);
~省略~

追加(2017/12/26)

もっとシンプルな方法で実装できますね,ということを5月頃には書こうと思っていたのですが,気がつけばもう2017年もあと僅か.
それでは早速コードを見てみましょう.

MyAsyncTask.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MyAsyncTask extends AsyncTask<Params, Progress, Result> {
    public MyAsyncTask() {
    }

    @Override
    protected void onPreExecute() {
    }

    @Override
    protected Result doInBackground(Params... params) {
        //並列処理
        //2つの処理を並列で行うので2を渡している
        ExecutorService service = Executors.newFixedThreadPool(2);
        Future<HogeData> future1 = service.submit(new HogeService(params[0]));
        Future<HogeData> future2 = service.submit(new HogeService(params[1]));
        try {
            //hogeData1とhogeData2の両方に値が入るまで,このスレッドは待機する
            HogeData hogeData1 = future1.get();
            HogeData hogeData2 = future2.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return result;
    }

    @Override
    protected void onPostExecute(Result result) {
    }
}
HogeService.java
import java.util.concurrent.Callable;

public class HogeService implements Callable<HogeData> {
    public HogeService(Params param) {
    }

    @Override
    public HogeData call() {
        //並列で行いたい処理
        return hogeData;
    }
}

補足

AsyncTaskには、キャンセル時の処理を記述するためのメソッドも用意されています。公式リファレンスなどを参照しながら実装するといいかと思います。

参考

AsyncTask | Android Developers
CountDownLatch | Android Developers
CountDownLatch (Java Platform SE 8 )
[Android] 何人かの非同期処理を待ち合わせる | アドカレ2013 : SP #23 | Developers.IO

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした