Android中如何正确使用协程(Coroutine)
Kotlin 协程(Coroutine)是一种轻量级的并发编程工具,在 Android 中尤为重要。它简化了异步任务的处理,使得代码更加简洁、可读且避免了回调地狱。在接下来系列的博客中,我们将介绍如何在 Android 中正确使用协程,并深入探讨协程的各种操作符(builders、scope、suspend 函数、launch、async 等)。
一、什么是协程?
协程是 Kotlin 的一种轻量级线程。它们与普通的线程不同,可以在一个线程中启动多个协程,协程之间的切换是非阻塞的。协程的运行效率高,并且能够简化复杂的异步代码流程。
这句话很重要哦:协程本质上是Kotlin语言包装的线程框架
1.1 协程的优势
- 轻量:与线程相比,协程的创建和切换开销极低。可以在一个线程内启动成千上万个协程。
- 可取消:协程可以被取消,这是异步任务处理中很重要的特性。
- 简化异步代码:协程将异步任务转换为顺序代码,避免了复杂的回调逻辑。
二、协程的基本构造器(Builders)
Kotlin 提供了几种主要的协程构造器:launch
、async
、runBlocking
等,每个构造器都有其特定的用途。
2.1 launch
launch
是最常用的协程构造器,它会启动一个新的协程并且不会返回任何结果,适用于不需要返回值的异步操作,如在后台执行一些任务。
val job = CoroutineScope(Dispatchers.IO).launch {
// 在后台线程执行
performLongRunningTask()
}
特点:
- 不会返回结果。
launch
返回的是一个Job
,可以用来取消任务。
2.2 async
async
用于执行并行任务并返回结果。它和 launch
的区别在于 async
会返回一个 Deferred
,这是一个延迟计算的值,通过 await()
可以获取执行结果。
val result = CoroutineScope(Dispatchers.IO).async {
// 返回计算结果
performLongRunningCalculation()
}.await()
特点:
- 返回一个
Deferred
对象,用于异步返回值。 - 可以在多个
async
中并发执行任务,并通过await
来等待结果。
2.3 runBlocking
runBlocking
是一个阻塞当前线程的协程构造器,它通常只在测试或者 main
函数中使用,避免在 Android 主线程中使用它。
runBlocking {
val result = performNetworkRequest()
println(result)
}
特点:
- 阻塞当前线程,直到协程完成。
- 在 Android 中避免使用
runBlocking
,因为它会阻塞主线程。
三、协程上下文和调度器(Dispatchers)
协程调度器定义了协程在什么线程中执行。Kotlin 为协程提供了以下几种调度器:
3.1 Dispatchers.Main
Dispatchers.Main
用于更新 UI,因为它运行在 Android 的主线程中。
CoroutineScope(Dispatchers.Main).launch {
// 在主线程中执行任务
updateUI()
}
3.2 Dispatchers.IO
Dispatchers.IO
专门用于 I/O 操作,如网络请求、数据库查询等,这些操作通常需要在后台线程中执行。
CoroutineScope(Dispatchers.IO).launch {
// 执行网络请求或数据库操作
val result = performNetworkRequest()
}
3.3 Dispatchers.Default
Dispatchers.Default
适用于 CPU 密集型任务,例如复杂计算等。
CoroutineScope(Dispatchers.Default).launch {
// 执行密集型计算
val result = performHeavyCalculation()
}
3.4 Dispatchers.Unconfined
Dispatchers.Unconfined
不指定任何线程,它会在调用它的线程上启动协程,但只会持续到第一个挂起点。
CoroutineScope(Dispatchers.Unconfined).launch {
// 随着挂起函数的切换,可能会在不同线程执行
log("Start")
delay(1000)
log("End")
}
四、挂起函数(Suspend Functions)
挂起函数(suspend
)是协程中最重要的概念之一。它们可以在协程中被调用,并在必要时“挂起”协程而不阻塞线程。当挂起的操作完成后,协程将继续执行。
suspend fun performNetworkRequest(): String {
delay(1000) // 模拟网络请求延迟
return "Request Result"
}
特点:
- 只能在协程或者另一个挂起函数中调用。
- 允许执行长时间运行的任务而不阻塞主线程。
五、协程作用域(Scope)和 Job
取消操作
协程作用域(CoroutineScope
)用于控制协程的生命周期。Android 中常用的协程作用域有 GlobalScope
和基于 ViewModel
的 viewModelScope
。
5.1 GlobalScope
GlobalScope
会启动一个顶层协程,它的生命周期和整个应用一样长,容易引发内存泄漏,因此不推荐使用。
GlobalScope.launch {
// 执行长期后台任务
}
5.2 viewModelScope
viewModelScope
是 Android 提供的一个用于 ViewModel
的协程作用域,协程会在 ViewModel
销毁时自动取消,非常适合执行与 UI 相关的任务。
class MyViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch {
val result = performNetworkRequest()
// 更新 LiveData 或 State
}
}
}
5.3 Job
的取消与超时
协程是可以被取消的。每个 launch
或 async
返回的都是一个 Job
,可以调用 job.cancel()
取消协程。
val job = CoroutineScope(Dispatchers.IO).launch {
performNetworkRequest()
}
job.cancel() // 取消协程
协程还可以设置超时时间,使用 withTimeout
来取消超时任务:
try {
withTimeout(5000) { // 设置5秒超时
performNetworkRequest()
}
} catch (e: TimeoutCancellationException) {
println("任务超时")
}
六、协程的其他操作符
6.1 withContext
withContext
用于切换协程上下文,它可以在协程中从一个调度器切换到另一个调度器。
suspend fun performTask() {
withContext(Dispatchers.IO) {
// 在后台线程执行
}
withContext(Dispatchers.Main) {
// 回到主线程更新UI
}
}
6.2 repeat
repeat
用于在协程中多次重复执行一个任务。
launch {
repeat(5) {
delay(1000)
println("Repeated $it")
}
}
七、正确使用协程的最佳实践
-
避免在
Activity
或Fragment
中使用GlobalScope
:使用GlobalScope
启动的协程生命周期与应用一样长,容易引发内存泄漏。推荐使用lifecycleScope
或viewModelScope
。 -
确保协程的取消机制:在涉及长时间运行任务的场景中,确保正确地处理协程的取消,以避免不必要的资源占用。
-
合理使用调度器:网络请求和数据库操作应该使用
Dispatchers.IO
,UI 操作应使用Dispatchers.Main
,而密集计算则应使用Dispatchers.Default
。 -
使用
suspend
函数简化异步代码:将复杂的异步操作封装在挂起函数中,确保代码的可读性和简洁性。
八、总结
通过合理地使用协程构造器、调度器、作用域以及取消机制,我们可以编写出更简洁、可维护的代码。掌握协程的各类操作符并将它们应用在实际项目中,你将会显著提高代码的可读性和效率。