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

所有 Android 應用程式都會使用主執行緒來處理 UI 作業。從這個主執行緒呼叫長時間執行的作業可能會導致畫面凍結及沒有回應。舉例來說,如果應用程式從主執行緒發出網路要求,應用程式的 UI 會停滯,直到收到網路回應為止。如果您使用 Java,可以建立額外的背景執行緒來處理長時間執行的作業,同時主執行緒繼續處理 UI 更新。

本指南說明使用 Java 程式設計語言的開發人員可以透過執行緒集區在 Android 應用程式中設定及使用多個執行緒。本指南也會說明如何定義要在執行緒上執行的程式碼,以及如何在這些執行緒和主要執行緒之間進行通訊。

並行程式庫

請務必瞭解執行緒的基本概念及其基礎機制。但是,許多熱門程式庫都針對這些概念提供更高層級的抽象層,以及現成的公用程式,用於在執行緒之間傳遞資料。這些程式庫包括 Java 程式設計語言使用者和適用的 GuavaRxJava,以及建議用於 Kotlin 使用者的協同程式

實際上,您應選擇最適合應用程式和開發團隊的做法,但執行緒的規則不變。

範例總覽

根據「應用程式架構指南」,這個主題中的範例會發出網路要求,並將結果傳回主執行緒,如此一來,應用程式便可在螢幕上顯示該結果。

具體來說,ViewModel 會呼叫主執行緒上的資料層,觸發網路要求。資料層負責將網路要求的執行作業移出主執行緒,並使用回呼將結果發布回主執行緒。

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

建立多個討論串

執行緒集區是代管的執行緒集合,可同時從佇列執行工作。當這些執行緒變成閒置狀態時,新工作就會在現有執行緒上執行。如要將工作傳送至執行緒集區,請使用 ExecutorService 介面。請注意,ExecutorService 與「服務」(Android 應用程式元件) 沒有關聯。

建立執行緒的費用高昂,因此在應用程式初始化時,建議您只建立一次執行緒集區。請務必將 ExecutorService 的執行個體儲存在 Application 類別或依附元件插入容器中。以下範例會建立四個執行緒的執行緒集區,由我們用來執行背景工作。

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

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

在背景執行緒中執行

如果對主執行緒發出網路要求,會讓執行緒等待或「封鎖」,直到收到回應為止。由於執行緒遭到封鎖,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() 方法採用 Runnable (可執行)。Runnable 是一種單一抽象方法 (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
                }
            }
        });
    }
}

在這個範例中,回呼是在呼叫執行緒 (背景執行緒) 中執行。也就是說,您必須切換回主執行緒,才能修改或直接與 UI 層通訊。

使用處理常式

您可以使用 Handler,將要對不同執行緒執行的動作排入佇列。如要指定執行動作的執行緒,請使用執行緒的迴圈建構 HandlerLooper 物件可用於執行相關執行緒的訊息迴圈。建立 Handler 後,即可使用 post(Runnable) 方法,在對應執行緒中執行程式碼區塊。

Looper 包含輔助函式 getMainLooper(),可擷取主執行緒的 Looper。您可以使用這個 Looper 建立 Handler,在主執行緒中執行程式碼。您可能會經常這麼做,因此也可以在儲存 ExecutorService 的相同位置儲存 Handler 的執行個體:

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() 與使用者介面通訊。

設定執行緒集區

您可以使用具備預先定義設定的其中一個 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
    );
    ...
}