使用 Java 线程异步工作

所有 Android 应用都使用主线程来处理界面操作。调用长时间运行的 此主线程中的操作可能会导致冻结和无响应。对于 例如,如果您的应用从主线程发出网络请求,那么应用界面 将被冻结,直到收到网络响应。如果您使用 Java 创建额外的后台线程来处理长时间运行的操作 主线程继续处理界面更新。

本指南介绍了使用 Java 编程语言的开发者如何使用 线程池来设置和使用 Android 应用中的多个线程。本指南 以及如何定义要在线程上运行的代码 在这些线程与主线程之间传输

请务必了解线程及其底层机制的基础知识。不过,也有许多热门库提供更高级别的 这些概念以及现成可用的实用程序来传递数据, 。这些库包括 GuavaRxJava(面向 Java 编程语言用户和协程) 我们建议 Kotlin 用户采用这种方法。

在实际操作中,您应该选择最适合您的应用和 但线程的规则保持不变。

示例概览

根据应用架构指南,本主题中的示例将 并将结果返回给主线程,应用随后 可能在屏幕上显示该结果。

具体而言,ViewModel 会调用主线程上的数据层, 触发网络请求。数据层负责移动 在主线程以外执行网络请求并发回结果 发送到主线程

为了将网络请求的执行移出主线程,我们需要 在我们的应用中创建其他线程。

创建多个线程

线程池是一个在各线程中运行任务的托管线程集合 并行处理。新任务在现有线程上执行, 线程变为空闲状态要将任务发送到线程池,请使用 ExecutorService 接口。请注意,ExecutorService 无需执行任何操作。 与 Android 应用组件 Service 配合使用。

创建线程的成本很高,因此您应该仅创建一次线程池, 您的应用初始化时。请务必保存 ExecutorService 的实例 在 Application 类或依赖项注入容器中。 以下示例创建了一个包含四个线程的线程池 运行后台任务。

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

您还可以通过其他方式配置线程池,具体取决于预期 工作负载如需了解详情,请参阅配置线程池

在后台线程中执行

在主线程上发出网络请求会导致该线程等待,或者 block,直到收到响应。由于线程处于阻塞状态,因此操作系统无法 调用 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() 方法采用 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 将要对其他 线程。要指定在哪个线程上运行操作,请构建 Handler,对线程使用 LooperLooper 是运行 相关会话的消息循环创建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() 与界面进行通信。

配置线程池

您可以使用其中一个 Executor 辅助函数创建线程池 如上一个示例代码所示。或者 如果您想自定义线程池的详细信息,可以创建一个 ThreadPoolExecutor 实例。您可以配置以下内容 详细信息:

  • 初始池大小和最大池大小。
  • 保持活跃的时间和时间单位。保持存活时间是 在关闭之前可以保持空闲状态
  • 包含 Runnable 任务的输入队列。此队列必须实现 BlockQueue 接口。为了符合应用的要求,您可以 从可用的队列实现中进行选择。如需了解详情,请参阅 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
    );
    ...
}