バックグラウンド スレッドでの Android タスクの実行

Android アプリはすべて、メインスレッドを使用して UI オペレーションを処理しますこのメインスレッドから長時間実行オペレーションを呼び出すと、フリーズして反応しなくなる可能性があります。たとえば、アプリがメインスレッドからネットワーク リクエストを行った場合、アプリの UI はネットワーク レスポンスを受信するまでフリーズします。メインスレッドが UI の更新処理を続けている間に、長時間実行オペレーションを処理する追加の「バックグラウンド スレッド」を作成できます。

このガイドでは、プログラミング言語 Kotlin と Java 両方のデベロッパーが「スレッドプール」を使用して、Android アプリで複数のスレッドを設定、使用する方法について説明します。また、スレッドで実行するコードを定義する方法と、こうしたスレッドとメインスレッドとの間で通信を行う方法についても説明します。

例の概要

このトピックの例は、アプリ アーキテクチャ ガイドに基づいて、ネットワーク リクエストを行い、結果をメインスレッドに返し、その結果をアプリが画面に表示します。

具体的には、ViewModel がメインスレッドのリポジトリ レイヤを呼び出してネットワーク リクエストをトリガーします。リポジトリ レイヤは、ネットワーク リクエストの実行をメインスレッドから移動し、コールバックを使用して結果をメインスレッドに戻す役割を担います。

ネットワーク リクエストの実行をメインスレッドから移動するには、アプリ内に他のスレッドを作成する必要があります。

複数スレッドの作成

スレッドプールは、キューからタスクを並列に実行する、スレッドの管理対象コレクションです。新しいタスクは、既存のスレッドがアイドル状態になると既存のスレッドで実行されます。タスクをスレッドプールに送信するには、ExecutorService インターフェースを使用します。なお ExecutorService は、Android アプリ コンポーネントである Services とは関係ありません。

スレッドの作成にはコストがかかるため、スレッドプールの作成はアプリ初期化時の 1 回だけにしてください。ExecutorService のインスタンスは、必ず Application クラスか 依存関係インジェクション コンテナに保存します。次の例では、バックグラウンド タスクの実行に使用できる 4 つのスレッドのスレッドプールを作成します。

Kotlin

class MyApplication : Application() {
    val executorService: ExecutorService = Executors.newFixedThreadPool(4)
}

Java

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

予想されるワークロードに応じてスレッドプールを構成する方法は他にもあります。詳細については、スレッドプールの構成をご覧ください。

バックグラウンド スレッドでの実行

メインスレッドでネットワーク リクエストを行うと、そのスレッドはレスポンスを受信するまで待機します(つまり、ブロックされます)。スレッドがブロックされるため、OS は onDraw() を呼び出せず、アプリがフリーズして、アプリケーション応答なし(ANR)のダイアログが表示される可能性があります。代わりに、このオペレーションをバックグラウンド スレッドで実行します。

まず、Repository クラスを見て、ネットワーク リクエストがどのように行われているかを確認します。

Kotlin

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; charset=utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())

            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

Java

// Result.java
public abstract class Result<T> {
    private Result() {}

    public static final class Success<T> extends Result<T> {
        public T data;

        public Success(T data) {
            this.data = data;
        }
    }

    public static final class Error<T> extends Result<T> {
        public Exception exception;

        public Error(Exception exception) {
            this.exception = exception;
        }
    }
}

// LoginRepository.java
public class LoginRepository {

    private final String loginUrl = "https://example.com/login";
    private final LoginResponseParser responseParser;

    public LoginRepository(LoginResponseParser responseParser) {
        this.responseParser = responseParser;
    }

    public Result<LoginResponse> makeLoginRequest(String jsonBody) {
        try {
            URL url = new URL(loginUrl);
            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
            httpConnection.setRequestMethod("POST");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setDoOutput(true);
            httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));

            LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
            return new Result.Success<LoginResponse>(loginResponse);
        } catch (Exception e) {
            return new Result.Error<LoginResponse>(e);
        }
    }
}

makeLoginRequest() は同期しており、呼び出し元のスレッドをブロックします。ネットワーク リクエストのレスポンスをモデル化するために、独自の Result クラスが用意されています。

ViewModel は、ボタンなどがタップされたときにネットワーク リクエストをトリガーします。

Kotlin

class LoginViewModel(
    private val loginRepository: LoginRepository
) {
    fun makeLoginRequest(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

Java

public class LoginViewModel {

    private final LoginRepository loginRepository;

    public LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody);
    }
}

前のコードでは、ネットワーク リクエストを行うときに、LoginViewModel がメインスレッドをブロックしています。インスタンス化したスレッドプールを使用して、実行をバックグラウンド スレッドに移動できます。まず、LoginRepository はコードを実行しており、スレッドを管理していないため、依存関係の注入の原則に従い、ExecutorService ではなく Executor のインスタンスを受け取ります。

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) { ... }

Java

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

Executor の execute() メソッドは Runnable を受け取ります。Runnable は、呼び出されたときにスレッド内で実行される run() メソッドを持つ、単一抽象メソッド(SAM)インターフェースです。

ではここで、実行をバックグラウンド スレッドに移動してレスポンスを無視する、makeLoginRequest() という別の関数を作成してみます。

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) {

    fun makeLoginRequest(jsonBody: String) {
        executor.execute {
            val ignoredResponse = makeSynchronousLoginRequest(url, jsonBody)
        }
    }

    private fun makeSynchronousLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        ... // HttpURLConnection logic
    }
}

Java

public class LoginRepository {
    ...
    public void makeLoginRequest(final String jsonBody) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
            }
        });
    }

    public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
        ... // HttpURLConnection logic
    }
    ...
}

execute() メソッド内で、バックグラウンド スレッドで実行するコードブロック(この例では同期ネットワーク リクエスト メソッド)を使用して新しい Runnable を作成します。内部的には、ExecutorServiceRunnable を管理し、利用可能なスレッドで実行します。

考慮事項

アプリ内のスレッドはメインスレッドを含む他のスレッドと並列に実行できるため、コードがスレッドセーフであることを確認する必要があります。この例では、スレッド間で共有される変数への書き込みを避け、代わりに不変データを渡しています。各スレッドが独自のデータ インスタンスで動作し、同期の複雑さを回避できるため、おすすめの方法です。

スレッド間で状態を共有する必要がある場合は、ロックなどの同期メカニズムを使用してスレッドからのアクセスを管理するように注意する必要があります。これはこのガイドの対象外です。通常は、変更可能な状態をスレッド間で共有しないでください。

メインスレッドとの通信

前のステップでは、ネットワーク リクエストのレスポンスを無視しました。結果を画面に表示するには、LoginViewModel に認識させる必要があります。そのために、コールバックを使用します。

関数 makeLoginRequest() は、非同期的に値を返せるように、コールバックをパラメータとして受け取る必要があります。結果のコールバックは、ネットワーク リクエストが完了するか、エラーが発生するたびに呼び出されます。Kotlin では高階関数を使用できます。しかし Java で同じ機能を持たせるには、新しいコールバック インターフェースを作成する必要があります。

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) {

    fun makeLoginRequest(
        jsonBody: String,
        callback: (Result<LoginResponse>) -> Unit
    ) {
        executor.execute {
            try {
                val response = makeSynchronousLoginRequest(jsonBody)
                callback(response)
            } catch (e: Exception) {
                val errorResult = Result.Error(e)
                callback(errorResult)
            }
        }
    }
    ...
}

Java

interface RepositoryCallback<T> {
    void onComplete(Result<T> result);
}

public class LoginRepository {
    ...
    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    callback.onComplete(result);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    callback.onComplete(errorResult);
                }
            }
        });
    }
  ...
}

ViewModel はコールバックを実装する必要があります。結果に応じて異なるロジックを実行できます。

Kotlin

class LoginViewModel(
    private val loginRepository: LoginRepository
) {
    fun makeLoginRequest(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody) { result ->
            when(result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

Java

public class LoginViewModel {
    ...
    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
            @Override
            public void onComplete(Result<LoginResponse> result) {
                if (result instanceof Result.Success) {
                    // Happy path
                } else {
                    // Show error in UI
                }
            }
        });
    }
}

この例で、コールバックは呼び出し元のスレッド(バックグラウンド スレッド)で実行されます。つまり、メインスレッドに切り替えるまで、UI レイヤを変更したり、UI レイヤと直接通信したりすることはできません。

ハンドラの使用

Handler を使用すると、別のスレッドで実行されるアクションをキューに登録できます。アクションを実行するスレッドを指定するには、スレッドの Looper を使用して Handler を作成します。Looper は、関連付けられたスレッドのメッセージ ループを実行するオブジェクトです。Handler を作成したら、post(Runnable) メソッドを使用して、対応するスレッドでコードブロックを実行できます。

Looper には、メインスレッドの Looper を取得するヘルパー関数 getMainLooper() があります。この Looper を使用して Handler を作成することで、メインスレッドでコードを実行できます。これは頻繁に行う可能性があるため、ExecutorService を保存した場所に Handler のインスタンスを保存することもできます。

Kotlin

class MyApplication : Application() {
    val executorService: ExecutorService = Executors.newFixedThreadPool(4)
    val mainThreadHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
}

Java

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

柔軟性が高まるため、ハンドラを Repository に挿入することをおすすめします。たとえば、今後別の Handler を渡して、別のスレッドでタスクをスケジュール設定することが考えられます。常に同じスレッドと通信している場合は、次の例に示すように、HandlerRepository コンストラクタに渡せます。

Kotlin

class LoginRepository(
    ...
    private val resultHandler: Handler
) {

    fun makeLoginRequest(
        jsonBody: String,
        callback: (Result<LoginResponse>) -> Unit
    ) {
          executor.execute {
              try {
                  val response = makeSynchronousLoginRequest(jsonBody)
                  resultHandler.post { callback(response) }
              } catch (e: Exception) {
                  val errorResult = Result.Error(e)
                  resultHandler.post { callback(errorResult) }
              }
          }
    }
    ...
}

Java

public class LoginRepository {
    ...
    private final Handler resultHandler;

    public LoginRepository(LoginResponseParser responseParser, Executor executor,
            Handler resultHandler) {
        this.responseParser = responseParser;
        this.executor = executor;
        this.resultHandler = resultHandler;
    }

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
    ...
}

あるいは、柔軟性を高める場合、各関数に Handler を渡せます。

Kotlin

class LoginRepository(...) {
    ...
    fun makeLoginRequest(
        jsonBody: String,
        resultHandler: Handler,
        callback: (Result<LoginResponse>) -> Unit
    ) {
        executor.execute {
            try {
                val response = makeSynchronousLoginRequest(jsonBody)
                resultHandler.post { callback(response) }
            } catch (e: Exception) {
                val errorResult = Result.Error(e)
                resultHandler.post { callback(errorResult) }
            }
        }
    }
}

Java

public class LoginRepository {
    ...

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler,
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback, resultHandler);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback, resultHandler);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
}

この例で、リポジトリの makeLoginRequest の呼び出しに渡されたコールバックは、メインスレッドで実行されます。つまり、コールバックから UI を直接変更することも、LiveData.setValue() を使用して UI と通信することもできます。

スレッドプールの設定

前のコード例で示したように、事前定義された設定で Executor ヘルパー関数のいずれかを使用して、スレッドプールを作成できます。あるいは、スレッドプールの詳細をカスタマイズする場合、ThreadPoolExecutor を直接使用してインスタンスを作成できます。次の詳細を設定できます。

  • 初期と最大のプールサイズ。
  • キープアライブ時間と時間単位。キープアライブ時間とは、スレッドがシャットダウンするまでにアイドル状態を維持できる最大の時間です。
  • Runnable タスクを保持する入力キュー。このキューは BlockingQueue インターフェースを実装する必要があります。アプリの要件に合わせて、利用可能なキューの実装から選択できます。詳細については、ThreadPoolExecutor のクラスの概要をご覧ください。

プロセッサコアの総数、キープアライブ時間 1 秒、入力キューに基づいてスレッドプール サイズを指定する例を示します。

Kotlin

class MyApplication : Application() {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors()

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private val workQueue: BlockingQueue<Runnable> =
            LinkedBlockingQueue<Runnable>()

    // Sets the amount of time an idle thread waits before terminating
    private const val KEEP_ALIVE_TIME = 1L
    // Sets the Time Unit to seconds
    private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS
    // Creates a thread pool manager
    private val threadPoolExecutor: ThreadPoolExecutor = ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    )
}

Java

public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}

同時実行ライブラリ

スレッド化の基本と、その基礎となるメカニズムを理解することは重要です。しかし、こうした概念を高度に抽象化し、スレッド間でデータを渡すためにすぐに使えるユーティリティを備えた、一般的なライブラリが多数あります。こうしたライブラリには、Java プログラミング言語ユーザー向けの GuavaRxJava、Kotlin ユーザーに推奨されるコルーチンが含まれます。

実際には、スレッド化のルールは変わりませんが、アプリと開発チームに最適なものを選択する必要があります。

関連ドキュメント

Android のプロセスとスレッドの詳細については、プロセスとスレッドの概要をご覧ください。