Java スレッドによる非同期処理

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

このガイドでは、Java プログラミング言語を使用するデベロッパーが スレッドプールを使用して、Android アプリで複数のスレッドをセットアップして使用します。このガイド スレッドで実行するコードを定義する方法と、API を介して メインスレッドの間で実行されるようにします

同時実行ライブラリ

スレッド化の基本と、その基礎となるメカニズムを理解することは重要です。ただし、より高レベルなインフラストラクチャを提供する一般的なライブラリも数多くあります。 抽象化や、データを受け渡すためのすぐに使えるユーティリティが 実行されます。これらのライブラリには、Guava と Java プログラミング言語ユーザー向け RxJavaコルーチン Kotlin ユーザーにおすすめします。

実践では、使用するアプリとアプリに最適なものを選ぶ必要があります。 開発チームと緊密に連携していましたが、スレッド化のルールは変わりません。

例の概要

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

具体的には、ViewModel がメインスレッドでデータレイヤーを呼び出して、 ネットワーク リクエストをトリガーします。データレイヤーはデータを メインスレッドからネットワーク リクエストを実行し、 メインスレッドにコールバックします。

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

複数のスレッドを作成する

スレッドプールは、Google Cloud 内でタスクを実行するスレッドのマネージド コレクションです。 並列処理を行えます新しいタスクは、既存のスレッドで実行するように アイドル状態になります。タスクをスレッドプールに送信するには、 ExecutorService インターフェース。なお、ExecutorService は何もしません Android アプリ コンポーネントであるサービスと統合します。

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

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

想定される状況に応じてスレッドプールを構成する方法は他にもあります。 学習します詳細については、スレッドプールの構成をご覧ください。

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

メインスレッドでネットワーク リクエストを行うと、スレッドが待機する。 ブロックします。スレッドがブロックされているため、OS は onDraw() を呼び出すとアプリがフリーズし、Application Not 応答中(ANR)ダイアログ代わりに、このオペレーションをバックグラウンドで実行しましょう。 使用します。

リクエストを行う

まず、LoginRepository クラスを見て、作成内容を確認します。 ネットワーク リクエスト:

// 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 は、ユーザーが ボタン:

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 を行います。 Executor のインスタンスを受け取りますが、ExecutorService ではありません。 スレッドを管理することではありません。

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

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

エグゼキュータの execute() メソッドは Runnable を受け取ります。Runnableとは 実行される run() メソッドがある Single Abstract Method(SAM)インターフェース 呼び出すことができます。

バックグラウンドで実行する

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

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 を管理し、 使用可能なスレッドで実行します。

考慮事項

アプリ内のどのスレッドも、他のスレッドと並行して実行できます。 コードがスレッドセーフであることを確認する必要があります。なお、 この例では、スレッド間で共有される変数への書き込みを避け、 不変データを使うことをおすすめします各スレッドは複数のスレッドに対応し、 独自のインスタンスを作成し、同期の複雑さを回避できるからです。

スレッド間で状態を共有する必要がある場合は、 スレッドからの CPU 使用率を 1% 未満に向上させることができます。これは次の範囲外です: これについては後ほど説明します通常は、変更可能な状態の共有を避ける必要があります。 スレッド間でやり取りできます。

メインスレッドとの通信

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

関数 makeLoginRequest() は、コールバックをパラメータとして受け取り、 非同期で値を返すことができます結果を返すコールバックは ネットワークリクエストが完了または障害が発生した場合に 再試行されますKotlin では、 高階関数を使いますただし、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 はコールバックを実装する必要があります。結果に応じて異なるロジックを実行できます。

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
                }
            }
        });
    }
}

この例では、コールバックは呼び出し元のスレッドで実行されます。 使用します。つまり、サービス アカウントを直接変更したり、 待機状態を維持する必要があります。

ハンドラを使用する

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

Looper にはヘルパー関数 getMainLooper() があります。この関数は、 メインスレッドの Looper。メインスレッドでコードを実行するには、 Looper: Handler を作成します。これはよくあることなので Handler のインスタンスを、ファイルを保存した場所と同じ場所に保存することもできます。 ExecutorService:

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

ハンドラをリポジトリに挿入することをおすすめします。これにより、 柔軟性が高まりますたとえば、将来的に、 異なる Handler を使用して、別のスレッドでタスクをスケジュールできます。いつも 同じスレッドに戻す場合は、Handler を 使用して作成します。

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 を 関数:

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 秒、入力キューで構成されています。

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
    );
    ...
}