使用 Java 執行緒進行非同步工作

所有 Android 應用程式都會使用主執行緒處理 UI 作業。長時間撥打電話 可能導致這個主要執行緒的作業沒有回應和沒有回應。適用對象 舉例來說,如果應用程式從主執行緒發出網路要求,應用程式的 UI 就會 就會凍結,直到收到網路回應為止。使用 Java 時 建立額外的背景執行緒來處理長時間執行的作業, 主執行緒繼續處理 UI 更新。

本指南說明使用 Java 程式設計語言的開發人員如何使用 執行緒集區:在 Android 應用程式中設定及使用多個執行緒。本指南 以及如何定義程式碼,以在執行緒上執行,以及如何 其中一個執行緒和主執行緒之間通訊。

並行程式庫

請務必瞭解執行緒的基本概念和其基礎 機制然而,很多熱門程式庫會提供 具備這些概念的抽象化機制,以及立即可用的公用程式來傳遞資料 。包括 GuavaRxJava 適用於 Java 程式設計語言使用者和協同程式, 我們推薦給 Kotlin 使用者

實務上,您應挑選最適合您應用程式的做法, 但執行緒的規則不變。

範例總覽

根據「應用程式架構指南」,本主題中的範例會根據 並將結果傳回至主執行緒,接著應用程式 可能會在畫面上顯示

具體來說,ViewModel 會呼叫主執行緒上的資料層, 觸發網路要求資料層會負責 從主執行緒執行網路要求,並將結果張貼回原始執行緒 呼叫至主執行緒。

如要將網路要求的執行作業移出主執行緒, 在應用程式中建立其他執行緒

建立多個討論串

執行緒集區是一組代管執行緒,會在 會從佇列平行處理。新工作會在現有執行緒上執行,就像任務一樣 執行緒就會處於閒置狀態。如要將工作傳送至執行緒集區,請使用 ExecutorService 介面。請注意,ExecutorService 沒有作用 利用服務,也就是 Android 應用程式元件。

建立執行緒的費用高昂,因此建議您僅建立執行緒集區一次, 應用程式初始化時。請務必儲存 ExecutorService 的例項 可在 Application 類別或依附元件插入容器中。 以下範例會建立四個執行緒的執行緒集區,以用於 然後執行背景任務

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

您還可以根據預期,使用其他方式設定執行緒集區 工作負載。詳情請參閱「設定執行緒集區」。

在背景執行緒中執行

對主要執行緒發出網路要求會導致執行緒等候,或 block,直到收到回應。執行緒遭到封鎖,因此 OS 無法 呼叫 onDraw(),進而導致應用程式凍結,可能導致應用程式未 回應 (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() 方法採用 RunnableRunnable 是一種 單一抽象方法 (SAM) 介面,其中 run() 方法會在以下環境執行: 執行緒。

在背景執行

讓我們建立另一個名為 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 我們要在背景執行緒中執行 以本例來說是同步網路 要求方法。ExecutorService 會在內部管理 Runnable 和 會在可用的執行緒中執行

注意事項

應用程式中的任何執行緒都能在其他執行緒 (包括主要執行緒) 並行執行。 因此請確保程式碼符合執行緒安全原則。請注意,在 相關範例,避免寫入執行緒之間共用的變數 而非不可變動的資料這是建議做法,因為每個執行緒都能 資料,並避免同步作業的複雜性。

如需在執行緒之間共用狀態,請務必謹慎管理存取權 從執行緒擷取自鎖定,如鎖定。這不在 是否採取其他策略一般來說,您應避免共用可變動狀態 執行緒之間的關聯

與主執行緒通訊

在上一個步驟中,我們忽略了網路要求回應。如要顯示 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 對執行緒使用迴圈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 的回呼 呼叫是在主執行緒上執行。這表示您可以直接修改 或使用 LiveData.setValue() 與 UI 通訊。

設定執行緒集區

您可以使用其中一個 Executor 輔助函式建立執行緒集區 並預先定義設定,如上一個程式碼範例所示另外 如要自訂執行緒集區的詳細資料,您可以建立 直接使用 ThreadPoolExecutor 的執行個體。您可以設定下列項目 詳細資料:

  • 集區初始大小和大小上限。
  • 保留運作時間和時間單位。存留時間是 執行緒可能在關閉之前就仍然處於閒置狀態。
  • 包含 Runnable 工作的輸入佇列。這個佇列必須實作 BlockingQueue 介面中。如要符合應用程式需求: 從可用的佇列實作選項中選擇若要瞭解詳情,請參閱課程 ThreadPoolExecutor 的總覽。

以下範例會根據 和輸入佇列。

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