Kotlin的重要优势及特点之——结构化并发
Kotlin 协程让异步代码像阻塞代码一样易于使用。协程可大幅简化后台任务管理,例如网络调用、本地数据访问等任务的管理。本主题介绍如何使用 Kotlin 协程解决以下问题,从而让您能够编写出更清晰、更简洁的应用代码。
所有源文件都必须编码为 UTF-8。
来源标注:Android 上的 Kotlin 协程 | Android Developers
书接上篇:Android Kotlin知识汇总(二)最佳实践-CSDN博客
Android 上的 Kotlin 协程
协程是一种并发设计模式,可以在 Android 平台上使用它来简化异步执行的代码。
在 Android 上,协程有助于管理长时间运行的任务。使用协程的专业开发者中有超过 50% 的人反映使用协程提高了工作效率。
简单来说,协程就是一种轻量级的非阻塞的线程工具API,可以用同步的方式写出异步的代码,优雅地切换线程和处理回调地狱。与线程的关系,线程在进程中,协程在线程中。
协程特点
协程是我们在 Android 上进行异步编程的推荐解决方案。值得关注的特点包括:
- 轻量:可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
- 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
- 内置取消支持:取消操作会自动在运行中的协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
主流创建协程的方式
使用协程时在代码写法上和普通的顺序代码类似。创建协程可以使用以下三种方式:
// 方法1:使用 runBlocking 顶层函数
runBlocking {}
// 方法2:使用 GlobalScope 单例对象,调用 launch 开启协程
GlobalScope.launch {}
// 方法3:创建 CoroutineScope 对象,调用 launch 开启协程
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {}
- 方法 1: 适用于单元测试场景,实际开发中不推荐,因为它是线程阻塞的;
- 方法 2: 不会阻塞线程,但它的生命周期会和 APP 一致,且无法取消;
- 方法 3: 推荐使用,可以通过 context 参数去管理和控制协程的生命周期。
使用协程确保主线程安全
通过launch()创建一个新的协程空间,{}内的代码块被叫做一个子协程。而传给 launch()的参数则用于指定执行这段代码运行的线程。
coroutineScope.launch(Dispatchers.IO) {//参数切到IO线程执行
}
coroutineScope.launch(Dispatchers.Main) {//参数切到主线程执行
}
在 Kotlin 中,所有协程都必须在调度程序中运行,即使它们在主线程上运行也是如此。
协程可以自行挂起,而调度程序负责将其恢复。
Kotlin 提供了三个调度程序,以用于指定应在何处运行协程:
- Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。
- Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。
- Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON。
使用 withContext 方法
该方法支持自动切回原来的线程,能够消除并发代码在协作时产生的嵌套。如果需要频繁地进行线程切换,这种写法将有很大的优势,即“使用同步的方式写异步代码”。如下所示:
coroutineScope.launch(Dispatchers.Main) {// Dispatchers.Main
val image = withContext(Dispatchers.IO) {// 切换到 IO 线程
getImage(imageUrl) // 切换到 IO 线程
}
imageView.setImageBitmap(image) // Dispatchers.Main
}
withContext(Dispatchers.IO)
创建一个在 IO 线程池中运行的代码块。放在该块内的任何代码都始终通过 IO
调度程序执行。由于 withContext
本身就是一个挂起函数,因此函数 getImage()
也是一个挂起函数。
使用 suspend 关键字
协程在常规函数的基础上添加了两项操作,用于处理长时间运行的任务。
suspend
用于暂停执行当前协程,并保存所有局部变量。resume
用于让已挂起的协程从挂起处继续执行。
代码在执行到某个 suspend 函数时会从正在执行它的线程上脱离,协程会从被挂起的 suspend 函数指定的线程(如 Dispatchers.IO)中开始执行。当该 suspend方法执行完成之后,会重新切换回它原先的线程。这个「切回来」的动作,在 Kotlin 中叫做 resume。
在上述示例中,把 withContext 单独放进一个getImage()里,并使用 suspend 关键字标记才能编译通过,示例代码如下:
suspend fun getImage(imageUrl: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* network IO here */
} // Dispatchers.Main
如果调用 suspend
函数,只能从其他 suspend
函数进行调用时,代码如下所示:
suspend fun fetchDocs() { // Dispatchers.Main
val result = getImage("url") // Dispatchers.IO
}
suspend fun getImage(url: String) = withContext(Dispatchers.IO) { /* ... */ }
在上面的示例中,getImage()
仍在主线程上运行,但它会在启动网络请求之前挂起协程。当网络请求完成时,getImage()
会恢复已挂起的协程fetchDocs(),而不是使用回调通知主线程。
获取协程的返回值
启动协程的方式有两种:
- launch 启动新协程而不将结果返回给调用方。
- async 在另一个协程内或在挂起函数内且在执行“并行分解”时才使用,并可以使用一个名为
await
的挂起函数返回结果。
警告:launch
和 async
处理异常的方式不同。由于 async
对 await
进行最终调用,因此它持有异常并将其作为 await
调用的一部分抛出。所以,使用 async
会有把异常静默吞掉的风险。
并行分解
基于Kotlin 的结构化并发机制,您可以定义启动一个或多个协程的
coroutineScope
。然后,您可以使用await()
或awaitAll()
保证这些协程在从函数返回结果之前完成。
例如,在一个coroutineScope里执行两个
并行的协程,此时
通过调用 await()
对每个延迟引用,就可以保证这两项 async
操作在返回值之前完成,代码如下所示:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
此外,coroutineScope
会捕获协程抛出的所有异常,并将其传送回调用方。
协程的非阻塞式挂起
「非阻塞式挂起」指的就是协程在挂起的同时切线程这件事情。使用了协程的代码看似阻塞,但由于协程内部做了很多工作(包括自动切换线程),它实际上是非阻塞的。
在代码执行的过程中,当线程执行到了 suspend 方法,就暂时不再执行剩余协程代码,跳出协程的代码块。如果它是一个后台线程,它会被系统回收或者再利用(继续执行别的后台任务),与 Java 线程池中的线程等同;如果它是 Android 主线程,它会继续执行界面刷新任务。
代码示例
使用协程模拟实现一个网络请求,等待时显示 Loading,请求成功或者出错让 Loading 消失,并将状态反馈给用户。
依赖项信息
如需在 Android 项目中使用协程,请将以下依赖项添加到应用的 build.gradle
文件中:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
ViewModel 代码
@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
enum class RequestStatus {
IDLE, LOADING, SUCCESS, FAIL
}
val requestStatus = MutableStateFlow(RequestStatus.IDLE)
/**
* 模拟网络请求
*/
fun simulateNetworkRequest() {
requestStatus.value = RequestStatus.LOADING
viewModelScope.launch {
val requestResult = async { performSimulatedRequest() }.await()
requestStatus.value =
if (requestResult) RequestStatus.SUCCESS else RequestStatus.FAIL
}
}
/**
* 模拟耗时操作,随机数->成功或失败
*/
private suspend fun performSimulatedRequest() =
withContext(Dispatchers.IO) {
delay(500)
val random = Random()
return @withContext random.nextBoolean()
}
}
MainActivity 代码
使用 Jetpack Compose,将请求状态实时显示在界面上。代码如下所示:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
//声明model属性
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTheme {
Surface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colorScheme.background) {
val requestStatusState =mainViewModel.requestStatus.collectAsState()
val requestStatus by rememberSaveable {requestStatusState }
Text(text = requestStatus.name,)
}
}
}
//请求网络
mainViewModel.simulateNetworkRequest()
}
}
小结
在 Kotlin 中,协程就是基于线程来实现的一种更上层的工具 API,在设计思想上,协程是一个基于线程的上层框架。Kotlin 协程并没有脱离 Kotlin 或者 JVM 创造新的东西,只是简化了多线程的开发。
下一篇,继续介绍 Kotlin 协程在 Jetpack 实战开发过程中最有用的一些方面。