又是一个月没见了,坚持永远是世上最难的事情,但,往往难事才会有更大的收获。与君共勉~
前段时间一直在学习 Compose,所以导致 Kotlin 笔记系列搁置了好久。一方面是因为 Compose 的学习在个人来看重要性更高;另一方面就是,发现学完之前的 Kotlin 系列的笔记一到笔记四后,已经基本可以在项目中使用 Kotlin 进行日常的编码了,所以才导致这个 Kotlin 学习笔记系列停更了好久,哈哈!对 Jetpack Compose 感兴趣的同学可以看一下我的另一个笔记系列—— Jetpack Compose 学习笔记。这次咱来看看 Kotlin 协程的基础知识。
1. 协程是什么
协程是一种编程思想。它并不局限于任何语言,不仅 Kotlin 中有对协程的实现,Python、Go 等语言也有。
更实际一点,协程的代码是运行在线程中的,可以在单线程中执行;也可以在多线程中执行,即支持来回切换。并且协程没有直接和操作系统关联,而是跟线程紧密关联,毕竟是要靠线程去执行。它的设计初衷就是为了解决并发问题,可以更方便地处理多线程协作的任务。
在 Kotlin 中,协程就是一个封装好的线程框架。类比于 Java 中的 Executor 或 Android 中的 AsyncTask。只要内存足够,一个线程可以运行任意多个协程,但在某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。下面图是进程、线程、协程之间的关系图:
这里是拿 Android 应用来举例的,其实不仅在 Android 中有 UI 主线程的概念,在 Go、Python 等支持协程的语言中,也有主线程的概念。有一点需要注意的是,进程是可以直接通过协程去处理一些事件的,只不过现在的编程语言都约定俗成地采用了图1 的这种层次。
所以,协程就是一套封装好的线程API框架,只不过使用起来非常方便,可以用看起来是同步的代码,去实现异步的操作。
2. suspend 关键字
先来看看 suspend 关键字,好像老是在协程的代码中碰到它。按照它的字面意思,就是“挂起”的意思,确实比较形象,因为被它修饰的方法,是可以被挂起的。这里被挂起的对象是这个方法所在的协程。那么,协程被挂起的真正意思是什么?
协程被挂起的意思是,这个正在线程上运行的协程体代码,将要从当前线程脱离开来,即剩下的协程代码不往下执行了。脱离开后,协程和线程会怎么样呢?线程这边比较好理解,如果有其他的任务需要处理,操作系统肯定会安排线程去执行其他的任务;如果暂时没有什么任务需要处理,可能就会被回收掉,或者放入线程池中;协程这一边,则将会在指定的线程中,继续执行之前被中断的代码。至于被指定的线程具体是哪个,是由 suspend 函数具体实现决定的。常见的可以调用 withContext 方法去指定线程。
suspend 关键字本身没有挂起的作用,需要在方法内部直接或者间接地调用 Kotlin 协程框架中的 suspend 函数才可以。所以,suspend 关键字更多的是给调用者一个提示,提示调用者被它修饰的方法是个耗时方法,需要在协程或者其他 suspend 函数中处理,限制这个方法只能在协程或其他 suspend 函数中被调用。
了解了 suspend 的用法,再来看看实际使用中,协程的几个组成部分。
3. 协程的几个重要组成部分
一般来说,协程的启动使用比较多的有如下的三个方法:
runBlocking: T
launch: Job
async/await: Deferred
通过查看这三个方法,我们可以得知协程的几个重要的属性,或者是参数。拿 launch: Job
的源码来说,我们从 GlobalScope.launch
的 launch 方法进入,可以看到 launch 的方法声明是:
// code 1 launch 方法声明 public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job
其中的 CoroutineContext
、CoroutineStart
、suspend CoroutineScope.() -> Unit
等几个参数都是协程的重要组成部分。
3.1 协程上下文
先看看 launch 方法的第一个参数—— CoroutineContext,协程上下文,跟 Android 里面的 Context 上下文类似,通常用于协程间切换时,传递参数的作用;还可以指定协程在哪个线程中执行,比如 IO 线程、UI Main 线程等;还可以指定当前协程中断后在哪个线程中去恢复它。
默认 CoroutineContext 是设置的 EmptyCoroutineContext —— 一个标准库已经定义好的 object,表示一个空的协程上下文,里面没有任何数据。而 CoroutineContext 的数据结构与集合类似,内部实现是一个单链表,里面的元素是 Element。
Element 中有一个属性 key,这个 key 就是元素 Element 的索引。要说协程上下文在我们的开发中如何使用,我找了下网上的一些资料,提到较多的就是异常的捕获了。如下面 code2 所示:
// code 2 设置协程的异常捕获 val coroutineExceptionHandler = CoroutineExceptionHandler { context, throwable -> // 出现异常则会执行 throwable.printStackTrace() } GlobalScope.launch (Dispatchers.Main + coroutineExceptionHandler) { // 执行操作 }
这段代码用到了 GlobalScope.launch,代表是个顶级作用域的协程,不推荐在 Android 开发中使用,因为它的生命周期是与Application 应用一致的,所以容易造成内存泄漏。CoroutineExceptionHandler 可以让我们在启动协程时设置一个统一的异常处理器,如果出现异常,就会执行相应的操作。这里的上下文还设置了协程运行的线程为 Main 主线程。此外,CoroutineContext 重载了 plus 方法(public operator fun plus(context: CoroutineContext): CoroutineContext
),所以可以直接使用加号 “+” 来添加一个 Element。
还有一个更为复杂的例子,也可以大致看出协程的组成:
// code 3 协程的内部组成 suspend { // 协程体 Log.d(TAG, "CoroutineStart: +++++ ${coroutineContext[CoroutineName]} ${Thread.currentThread().name}") // val tmp = 1/0 "suspend 返回值" }.startCoroutine(object : Continuation<String> { override val context: CoroutineContext get() = EmptyCoroutineContext + CoroutineName("修之竹") + CoroutineExceptionHandler { context, throwable -> Log.d(TAG, "CoroutineExceptionHandler: ++++ ${throwable}") } override fun resumeWith(result: Result<String>) { // 协程执行完会执行 resumeWith 方法 Log.d(TAG, "resumeWith: ++++ CoroutineEnd") result.onSuccess { Log.d(TAG, "++++++ resumeWith onSuccess: ${context[CoroutineName]?.name}") } result.onFailure { Log.d(TAG, "++++++ resumeWith onFailure") context[CoroutineExceptionHandler]?.handleException(context, it) } } })
首先,被 suspend 包裹的代码段就是协程需要去执行的协程体,最后还有一个返回值。其次,startCoroutine 方法中的匿名内部类 Continuation 实际上实现了协程上下文的配置以及协程执行完的回调。
在配置协程上下文中,使用了 CoroutineName 和 CoroutineExceptionHandler 添加了两个元素。CoroutineName 可以为协程绑定一个名字;CoroutineExceptionHandler 前文已有说明。
而 resumeWith 方法就是协程的回调方法,执行失败或完成都会回调,就拿上面的代码,在Activity onCreate 方法中执行,就会输出下面的信息:
可以看出,通过 CoroutineName 确实可以给协程绑定一个名字,而且在协程体中可通过 coroutineContext 协程上下文对象获取到协程上下文的一些信息;协程执行完成时,回调的是 resumeWith 中 Result 的 onSuccess 方法;协程执行出错时,回调的是 resumeWith 中 Result 的 onFailure 方法。如果把 code 3 中的 val tmp = 1/0
去掉注释再运行,则会输出下面的情况:
协程上下文就说到这里。