所有 Android 应用都使用主线程来处理界面操作。从此主线程调用长时间运行的操作可能会导致冻结和无响应。例如,如果应用从主线程发出网络请求,应用的界面会处于冻结状态,直到它收到网络响应。您可以创建额外的后台线程来处理长时间运行的操作,同时主线程继续处理界面更新。
本指南向 Kotlin 和 Java 编程语言开发者介绍了如何借助线程池在 Android 应用中设置和使用多个线程。此外,本指南还介绍了如何定义要在线程上运行的代码,以及如何在其中一个线程与主线程之间进行通信。
示例概览
根据应用架构指南,本主题中的示例会发出网络请求并将结果返回到主线程,然后应用可能会在主线程上将该结果显示在屏幕上。
具体而言,ViewModel
会在主线程上调用代码库层以触发网络请求。代码库层负责将网络请求的执行任务移出主线程,并使用回调将结果发布回主线程。
为了将网络请求的执行任务移出主线程,我们需要在应用中创建其他线程。
创建多个线程
线程池是在队列中并行运行任务的托管线程集合。当现有线程变为空闲状态时,新任务会在这些线程上执行。如需将任务发送到线程池,请使用 ExecutorService
接口。请注意,ExecutorService
与服务(即 Android 应用组件)无关。
创建线程的成本很高,因此您应在应用初始化时仅创建一次线程池。请务必将 ExecutorService
的实例保存在 Application
类或依赖项注入容器中。以下示例创建了一个包含四个线程的线程池,我们可以用其运行后台任务。
Kotlin
class MyApplication : Application() { val executorService: ExecutorService = Executors.newFixedThreadPool(4) }
Java
public class MyApplication extends Application { ExecutorService executorService = Executors.newFixedThreadPool(4); }
您可以通过其他方式配置线程池,具体取决于预期工作负载。如需了解详情,请参阅配置线程池。
在后台线程中执行
在主线程上发出网络请求会导致该线程处于等待或阻塞状态,直到它收到响应。由于该线程处于阻塞状态,因此操作系统无法调用 onDraw()
,于是应用会冻结,这样有可能导致出现应用无响应 (ANR) 对话框。我们改为在后台线程上运行此操作。
首先,我们来了解一下 Repository
类,看看它是如何发出网络请求的:
Kotlin
sealed class Result<out R> { data class Success<out T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() } class LoginRepository(private val responseParser: LoginResponseParser) { private const val loginUrl = "https://example.com/login" // Function that makes the network request, blocking the current thread fun makeLoginRequest( jsonBody: String ): Result<LoginResponse> { val url = URL(loginUrl) (url.openConnection() as? HttpURLConnection)?.run { requestMethod = "POST" setRequestProperty("Content-Type", "application/json; charset=utf-8") setRequestProperty("Accept", "application/json") doOutput = true outputStream.write(jsonBody.toByteArray()) return Result.Success(responseParser.parse(inputStream)) } return Result.Error(Exception("Cannot open HttpURLConnection")) } }
Java
// 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
会在用户点按(例如,点按按钮)时触发网络请求:
Kotlin
class LoginViewModel( private val loginRepository: LoginRepository ) { fun makeLoginRequest(username: String, token: String) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) } }
Java
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
的实例,因为它是在执行代码,而不是管理线程:
Kotlin
class LoginRepository( private val responseParser: LoginResponseParser private val executor: Executor ) { ... }
Java
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()
的函数,该函数会将执行任务移至后台线程,并暂时忽略响应:
Kotlin
class LoginRepository( private val responseParser: LoginResponseParser private val executor: Executor ) { fun makeLoginRequest(jsonBody: String) { executor.execute { val ignoredResponse = makeSynchronousLoginRequest(url, jsonBody) } } private fun makeSynchronousLoginRequest( jsonBody: String ): Result<LoginResponse> { ... // HttpURLConnection logic } }
Java
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 中,我们必须创建一个新的回调接口,以实现相同的功能:
Kotlin
class LoginRepository( private val responseParser: LoginResponseParser private val executor: Executor ) { fun makeLoginRequest( jsonBody: String, callback: (Result<LoginResponse>) -> Unit ) { executor.execute { try { val response = makeSynchronousLoginRequest(jsonBody) callback(response) } catch (e: Exception) { val errorResult = Result.Error(e) callback(errorResult) } } } ... }
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
现在需要实现回调。它可以根据结果执行不同的逻辑:
Kotlin
class LoginViewModel( private val loginRepository: LoginRepository ) { fun makeLoginRequest(username: String, token: String) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) { result -> when(result) { is Result.Success<LoginResponse> -> // Happy path else -> // Show error in UI } } } }
Java
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
保存在同一位置:
Kotlin
class MyApplication : Application() { val executorService: ExecutorService = Executors.newFixedThreadPool(4) val mainThreadHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper()) }
Java
public class MyApplication extends Application { ExecutorService executorService = Executors.newFixedThreadPool(4); Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper()); }
最好将处理程序注入 Repository
,因为这样可以提高灵活性。例如,您将来可能希望传入不同的 Handler
,以在单独的线程上调度任务。如果您始终将结果传回同一线程,则可以将 Handler
传入 Repository
构造函数,如以下示例所示。
Kotlin
class LoginRepository( ... private val resultHandler: Handler ) { fun makeLoginRequest( jsonBody: String, callback: (Result<LoginResponse>) -> Unit ) { executor.execute { try { val response = makeSynchronousLoginRequest(jsonBody) resultHandler.post { callback(response) } } catch (e: Exception) { val errorResult = Result.Error(e) resultHandler.post { callback(errorResult) } } } } ... }
Java
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); } }); } ... }
或者,如果您需要更高的灵活性,则可以向每个函数传入一个处理程序:
Kotlin
class LoginRepository(...) { ... fun makeLoginRequest( jsonBody: String, resultHandler: Handler, callback: (Result<LoginResponse>) -> Unit ) { executor.execute { try { val response = makeSynchronousLoginRequest(jsonBody) resultHandler.post { callback(response) } } catch (e: Exception) { val errorResult = Result.Error(e) resultHandler.post { callback(errorResult) } } } } }
Java
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
的类概览。
以下示例根据处理器核心总数指定了线程池大小,指定了保持活跃的时间为一秒,并指定了一个输入队列。
Kotlin
class MyApplication : Application() { /* * Gets the number of available cores * (not always the same as the maximum number of cores) */ private val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors() // Instantiates the queue of Runnables as a LinkedBlockingQueue private val workQueue: BlockingQueue<Runnable> = LinkedBlockingQueue<Runnable>() // Sets the amount of time an idle thread waits before terminating private const val KEEP_ALIVE_TIME = 1L // Sets the Time Unit to seconds private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS // Creates a thread pool manager private val threadPoolExecutor: ThreadPoolExecutor = ThreadPoolExecutor( NUMBER_OF_CORES, // Initial pool size NUMBER_OF_CORES, // Max pool size KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, workQueue ) }
Java
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 ); ... }
并发库
请务必了解线程及其底层机制的基础知识。不过,有许多热门库提供了在这些概念基础之上的更高级别的抽象概念,以及用于在线程之间传递数据的现成实用程序。这些库包括面向 Java 编程语言用户的 Guava 和 RxJava,以及我们推荐给 Kotlin 用户的协程。
在实际操作中,您应挑选最适合您的应用和开发团队的库,但线程的规则保持不变。
其他资源
如需详细了解 Android 中的进程和线程,请参阅进程和线程概览。