一、kotlin协程关键字
常用关键字如下:
// 作用域相关
GlobalScope,coroutineScope,viewModelScope(Android),lifecycleScope(Android)
// 启动协程相关
launch,async
// 挂起函数
suspend
// 切换协程执行的线程
withContext,Dispatchers.Default,Dispatchers.IO,Dispatchers.Main
协程解决了两个核心问题:
- 同步方法写代码
- 自动线程切换
Kotlin 协程如何实现的呢?如果看过 JavaScript 的 Promise 相关的代码进行语法脱糖处理,那就很好理解了,核心也是通过生成状态机代码。
二、使用协程
要使用协程来运行任务,我们首先需要问问自己这三个问题,搞懂了立马可以写出代码。
1. 任务需要跟随指定场景取消吗?
不需要选择GlobalScope,跟随Activity选择lifecycleScope,跟随ViewModel选择viewModelScope。
2. 任务是否需要返回值?
不需要选择launch,需要选择async。
3. 任务执行在哪个线程?
- 主线程使用
withContext(Dispatchers.Main) - 网络和IO任务使用
withContext(Dispatchers.IO) - CPU 密集型任务选择
withContext(Dispatchers.Default)
4、实际应用
比如我要执行一个文件写入任务,是个全局任务(选GlobalScope),不要返回值(选launch),运行在 IO 线程(选withContext(Dispatchers.IO))。代码如下:
fun writeSomething() { }
GlobalScope.launch {
withContext(Dispatchers.IO) {
writeSomething()
}
}
三、理解协程作用域
3.1 作用域有啥用?
GlobalScope,coroutineScope,viewModelScope(Android),lifecycleScope(Android)都是协程作用域。 作用域是干啥用的呢? 首先,协程必须运行在作用域内。作用域主要有如下几个作用:
- 生命周期管理
作用域内的协程会随作用域的取消而自动取消,避免内存泄漏。比如Android的lifecycleScope会随着Activity销毁自动取消所有的协程。
- 结构化并发
通过父子协程关系实现任务分组:父协程取消时,所有子协程也会取消;父协程会等待所有子协程完成。
- 上下文传递
子协程默认继承父协程的上下文(如调度器、异常处理器),可覆盖特定元素(如切换线程)
- 异常传播
协同作用域(coroutineScope):子协程异常会取消父协程和兄弟协程; 主从作用域(supervisorScope):子协程异常不影响其他协程
我是这样理解作用域的:作用域就是个协程的 Manager,每个协程都必须处在 Manager 的管理中。一个 Manager 中可以有多个协程,如果 Manager 被取消了,所有的协程都会被取消。 因此在使用协程时,第一个要想的就是挑选一个合适的作用域。以下是各个作用域的介绍。
-
GlobalScope
:全局的。只要应用程序还在就不会被取消。适合长期运行的后台任务(不推荐常用)。
-
lifecycleScope
:绑定 Activity/Fragment 生命周期,自动取消
-
viewModelScope
:绑定 ViewModel 生命周期,避免因配置变更(如屏幕旋转)重复执行任务
3.2 自定义作用域
如果我想自定义个协程作用域,怎么做呢?
3.2.1 工厂函数自定义
// 示例:自定义作用域(IO调度器 + SupervisorJob + 异常处理)
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("Caught exception: $throwable")
}
val customScope = CoroutineScope(
SupervisorJob() + Dispatchers.IO + exceptionHandler
)
// 使用
customScope.launch {
// 协程逻辑K
}
3.2.2 继承CoroutineScope
class MyViewModel : CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext = job + Dispatchers.Main
fun fetchData() {
launch {
// 协程逻辑
}
}
fun onDestroy() {
job.cancel() // 取消所有协程
}
}K
3.2.3 临时作用域
临时作用域指coroutineScope和supervisorScope,这两个只能在suspend函数中使用。suspend函数后面介绍
suspend fun fetchAllData() = coroutineScope {
launch { fetchData1() }
launch { fetchData2() } // 若失败,会取消父协程
}
suspend fun downloadFiles() = supervisorScope {
launch { downloadFile("url1") } // 失败不影响其他下载
launch { downloadFile("url2") }
}
四、启动协程
有了作用域就可以启动一个协程了。这里主要用到两个接口:launch,async,怎么选呢? 口诀就是需要返回值用async,不需要用 launch。
4.1 launch
launch返回一个Job,我理解为是启动协程的一个句柄。注意Job不是任务的返回值,而是启动协程后,能够控制协程cancel,join的句柄
val job = launch {
doBackgroundTask() // 无返回值
}
job.cancel() // 可取消任务
4.2 async
async返回Deferred,是 Job 的子类。Deferred同样不是任务的返回值,那么为啥说async是需要返回值时使用呢?请看代码
suspend fun fetchData(): NetData {
}
val deferred = async {
fetchData() // 返回结果
}
val netData = deferred.await() // 通过 await 获取到 fetchData 函数的返回值。
五、挂起函数
用suspend修饰的函数就是挂起函数,该函数必须在协程作用域内或者其他挂起函数中调用。 Kotlin 协程设计suspend函数的核心目的是以同步代码风格实现异步非阻塞操作,同时解决传统异步编程的复杂性。 说那么多,到底怎么用呢?忽略那么多概念,如果想定义一个 suspend 函数相当简单
// 加个 suspend 搞定!
suspend fun fetchData() {
}
那么什么时候用呢?所有的耗时操作封装的函数都可以加!比如数据库读取,网络请求,写文件等。像 Room 和 Retrofit 都官方支持了
@Dao
interface TitleDao {
// Room 支持
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
}
suspend fun writeData(data: String) {
// 写文件
}
为啥要用呢?
我们知道所有的协程都要在作用域内执行,如果封装的方法都要去指定作用域多麻烦啊,而且封装时也不知道方法会在哪个作用域调用,作用域可能还没创建出来呢。因此使用suspend修饰函数就巧妙解决了这个问题,因为suspend函数只能在作用域或者另一个suspend函数中执行,这就保证了suspend函数最后一定能在一个作用域中执行。
另外,suspend函数内可以看做已经有作用域了,那么执行其他suspend函数就易如反掌。
最后,来看看suspend函数带来的代码优化,可以想想如果用常规线程+回调的方法是不是代码量和可读性都复杂了好多。
suspend fun fetchDataFromNet(): String {
}
suspend fun saveDataToDB(data: String) {
}
// 同步方式写异步代码!自动线程切换!= 简洁易懂的代码
suspend fun showData2View() {
viewModelScope.launch {
val data = fetchDataFromNet()
saveDataToDB(data)
showDataView(data)
}
}
六、调度器
协程到底是运行在哪个线程呢?一开始看一些博客的时候,是云里雾里的。感觉好多地方写的语焉不详。 我觉得不管协程是怎么高深的东西,肯定脱离不了操作系统对于进程和线程的定义。最小的任务执行单元肯定是线程,那么协程就必须有其执行线程! 这就是调度器的作用了。调度器指定了协程运行的线程。
suspend fetchData() {
// 运行在单独的线程,Kotlin 封装的 IO 线程池
withContext(Dispatchers.IO) {
// do NetWork request
}
}
suspend parseJson() {
// 运行在单独的线程,Kotlin 封装的 CPU 密集型线程池
withContext(Dispatchers.Default) {
// parse
}
}
所以我们想要将任务运行到指定的线程时,可以使用withContext + Dispatchers进行切换。withContext 里头执行在指定的线程,执行时外部线程是挂起的,执行完外部线程恢复。
天知道这个特性有多好用。 以下是一个典型的例子:
suspend fun fetchUserInfo(): UserInfo {
return withContext(Dispatchers.IO) {
// do NetWork request // IO 线程
}
}
fun showUserInfoView() {
viewModelScope.launch {
showLoadingView() // 主线程
val userInfo = fetchUserInfo()
dimissLoadingVeiw() // 主线程
userInfoView.show(userInfo) // 主线程
}
}
首先作用域是有指定线程的,如果没有特别指定,那么协程运行在作用域指定的线程中。比如viewModelScope指定了主线程,那么所有该作用域启动协程默认都是在主线程。 如果想要切换,使用withContext + Dispatchers,切换过程中,Kotlin 帮忙处理了线程切换的工作(协程叫挂起和恢复)。