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