Kotlin协程理解

一、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)都是协程作用域。 作用域是干啥用的呢? 首先,协程必须运行在作用域内。作用域主要有如下几个作用:

  • 生命周期管理

作用域内的协程会随作用域的取消而自动取消,避免内存泄漏。比如AndroidlifecycleScope会随着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 临时作用域

临时作用域指coroutineScopesupervisorScope,这两个只能在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不是任务的返回值,而是启动协程后,能够控制协程canceljoin的句柄

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 帮忙处理了线程切换的工作(协程叫挂起和恢复)。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容