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
を作成します。内部的には、ExecutorService
が Runnable
を管理し、利用可能なスレッドで実行します。
考慮事項
アプリ内のスレッドはメインスレッドを含む他のスレッドと並列に実行できるため、コードがスレッドセーフであることを確認する必要があります。この例では、スレッド間で共有される変数への書き込みを避け、代わりに不変データを渡しています。各スレッドが独自のデータ インスタンスで動作し、同期の複雑さを回避できるため、おすすめの方法です。
スレッド間で状態を共有する必要がある場合は、ロックなどの同期メカニズムを使用してスレッドからのアクセスを管理するように注意する必要があります。これはこのガイドの対象外です。通常は、変更可能な状態をスレッド間で共有しないでください。
メインスレッドとの通信
前のステップでは、ネットワーク リクエストのレスポンスを無視しました。結果を画面に表示するには、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
を渡して、別のスレッドでタスクをスケジュール設定することが考えられます。常に同じスレッドと通信している場合は、次の例に示すように、Handler
を Repository
コンストラクタに渡せます。
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 プログラミング言語ユーザー向けの Guava と RxJava、Kotlin ユーザーに推奨されるコルーチンが含まれます。
実際には、スレッド化のルールは変わりませんが、アプリと開発チームに最適なものを選択する必要があります。
関連ドキュメント
Android のプロセスとスレッドの詳細については、プロセスとスレッドの概要をご覧ください。