所有 Android 应用都使用主线程来处理界面操作。从此主线程调用长时间运行的操作可能会导致冻结和无响应。例如,如果您的应用从主线程发出网络请求,应用的界面就会冻结,直到收到网络响应。如果您使用 Java,则可以创建额外的后台线程来处理长时间运行的操作,同时主线程继续处理界面更新。
本指南介绍了使用 Java 编程语言的开发者如何利用线程池在 Android 应用中设置和使用多个线程。本指南还介绍了如何定义要在线程上运行的代码,以及如何在其中一个线程与主线程之间进行通信。
并发库
请务必了解线程及其底层机制的基础知识。不过,有许多热门库提供对这些概念的更高级抽象,以及用于在线程之间传递数据的现成实用程序。这些库包括面向 Java 编程语言用户的 Guava 和 RxJava,以及我们建议 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;
}
...
}
执行器的 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
}
}
});
}
}
在此示例中,回调在发起调用的线程中执行,该线程是后台线程。这意味着,在切换回主线程之前,您无法直接修改界面层或与界面层进行通信。
使用处理程序
您可以使用 Handler 将要在其他线程上执行的操作加入队列。如需指定要在哪个线程上运行操作,请使用该线程的 Looper 构造 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()
与界面进行通信。
配置线程池
您可以使用某个具有预定义设置的 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
);
...
}