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

根据预期的工作负载,您可以通过其他方式配置线程池。有关更多信息,请参阅配置线程池

在后台线程中执行

在主线程上发出网络请求会导致线程等待或阻塞,直到它收到响应。由于线程被阻塞,因此操作系统无法调用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;
    }
    ...
}

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

在此示例中,回调在调用线程(即后台线程)中执行。这意味着在切换回主线程之前,您无法直接修改或与 UI 层通信。

使用处理程序

您可以使用 Handler 将要在线程上执行的操作排队。要指定要在其上运行操作的线程,请使用该线程的 Looper 构造 HandlerLooper 是一个对象,它为关联的线程运行消息循环。创建 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
    );
    ...
}