协程 Coroutines,指各任务协作运行;
线程是操作系统层面的,由操作系统调度执行,我们可以开启一个线程,但无法知道线程什么时候执行,什么时候执行完,因此我们通常使用回调的形式在线程执行完之后接受执行的结果,线程的运行是抢占式的,后起的 B 线程可能抢占先起的 A 线程的资源,A 线程会被阻塞,从而造成资源的浪费。
协程是应用层面的,它由虚拟机进行调度,我们可以随意开启和终止协程的运行,协程是非抢占氏的,如果当前协程在运行,除非当前运行的协程主动 退让,挂起,否则其他协程不会抢占运行机会,由于各任务写作运行,就避免了创建大量的线程。
协程本身并不具备线程切换的功能,耗时操作等仍旧需要我们手动切换到子线程执行,但是协程的 API 设计使得我们可以像编写同步代码一样编写异步代码,避免使用回调,逻辑也更清晰。
本文中的代码没有真正的进行操作,使用输出 log 来代替
推荐阅读
使用回调进行异步
使用回调接口获取异步结果,是在 Java 中常用的方法,在主线程进行 UI 操作,切换到子线程进行耗时操作,操作的结果返回到主线程更新 UI。
1  | // 线程池  | 
因为我们只有一层回调,加上 Kotlin 对 Lambda 表达式的支持,看起来代码还不是那么难看,这也是在 Java 中常用的方法,相对好理解一些,看一下输出结果:
1  | E/Coroutine: main 开始先来个UI操作  | 
使用协程实现
协程借助 suspend 关键字实现,使用 suspend 关键字描述的函数,表示此函数可以被挂起。
介绍几个概念
CoroutineContext,协程上下文,很多框架都会有一个上下文对象,支持 + 操作符,因此它可以是多个上下文的累加结果,Kotlin 中实现了一个空的实现 EmptyCoroutineContext,用来占位。
Continuation,协程中的任务执行后的操作,它可以返回任务的结果或者异常。
同样使用协程来实现一个和上面类似的功能:
1  | fun testCoroutine() {  | 
输出结果:
1  | E/Coroutine: main 开始先来个UI操作  | 
我们看到,最后结果的 UI 操作,还是在子线程跑的,这个是不允许的,但是从代码上面,最后打印结果的代码明明是在主线程,其实 var resule = xx 之后的代码就如同回调中的代码,它的运行环境取决于 Continuation 的 resume() 方法执行的环境,在这段协程的代码中,最关键的一句是 
1  | val result = doLotsOfThingsCanSuspend("test")  | 
它替代了原先的回调方法,也实现了类似原来回调的功能,当运行到这边时,函数会被挂起,等结果返回之后才会赋值并且进行后面的操作,避免了层层回调嵌套,另外虽然最后拿到结果的代码看起来是写在主线程,其实最后运行在了子线程,因此我们可以如下,修改代码,保证最后的结果被传递回主线程。
1  | // 一个耗时的方法,可能被挂起  | 
但是对于这个操作,借助 Continuation 和 CoroutineContext 可以有更优雅的实现。
将结果返回切换到主线程
写一个  Continuation 的包装类 UIContinuation,这个很好理解,一个典型的包装者模式,他包装其他的 Continuation 在调用 resume() 方法时将结果返回主线程。
1  | class UIContinuation<in T>(val delegate: Continuation<T>) : Continuation<T> {  | 
重写 CoroutineContext 自动完成 Continuation 的包装,需要一个接收外界设置的 CoroutineContext 的 Continuation,他其实什么也不需要做。
1  | class ContextContinuation(override val context: CoroutineContext = EmptyCoroutineContext) : Continuation<Unit> {  | 
重写 CoroutineContext 使用插值器的方式包装,这里使用了 ContinuationInterceptor,他也是 CoroutineContext 的子类,类似 okHttp 的 interceptor,他的作用就是,拦截旧的 Continuation 如果需要的话生成新的 Continuation.
1  | class AsyncCoroutineContext :  | 
此时,调用如下方法启动协程,配置可以将结果返回主线程的上下文,当结果返回时会自动返回主线程。
1  | fun callCoroutine(block: suspend () -> Unit) {  | 
使用 CoroutineContext 传参
首先,CoroutineContext 有一个不错的特性,就是重载了 + 操作符,因为我们可以对 CoroutineContext 进行加法操作返回新的 CoroutineContext
为了方便使用,我们将基础的方法进行提取,他们实现一个通用的功能,即切换后台线程执行耗时操作,然后在主线程接收结果更改UI。
1  | // 开始协程,可以从外界接受上下文变量  | 
借助上面的抽象出来的两个方法可以开启协程,执行任意耗时操作,并将结果返回到主线程,接下来使用 ConroutineContext 进行参数的传递,从上面我们发现 block 函数是一个无参的函数,而各种耗时操作参数不一,怎么兼容这个问题,就需要使用 ConroutineContext ,他是一个上下文,是整个运行环境,借助它我们可以传递参数,定义一个携带参数的上下文,它仍旧是 AbstractCoroutineContextElement 的子类,并标记一个 Key
1  | // 携带上下文的 context  | 
从上下文中获取参数,重点在于 this[ParamContext.Key],也可以写作 this[ParamContext],因为那个 Key 是一个伴生对象,能用下标访问是因为 CoroutineContext 重载了 [] 操作符,从而可以取出之前传递的 ParamContext
1  | fun testCoroutine() {  | 
Sequence
官方的一个例子
1  | val fibonacci = buildSequence {  | 
每次对 Sequence 迭代遍历下一个时,会执行协程到 yield 的位置返回值,并挂起,等待下一次迭代,这样就构造了一个懒序列,只有在需要的时候才会计算生成,而不是一次生成大量数据,除了第一次,他的每次被调起,都是从这次 yield 到下次 yield
总结
使用 suspendCoroutine 构造一个可挂起的函数,在内部执行操作,通过 Continuation 的 resume() 方法将结果或者异常返回,你可以决定返回之前是不是要进行自己的处理,比如发到主线程。
Continuation 是一个接口,他是一个结果的接受处理者,这也是 Continuation 的含义,他将在任务完成之后被调用。
CoroutineContext 是一个上下文,表示一个运行时环境,它可以做很多事,比如传递参数等。
ContinuationInterceptor 是 Continuation 的插值器,提供一个更改原来的 Continuation 的机会,用来创建返回新的 Continuation。
从代码层面上面,可以跟回调的方法稍作比较,下面代码中,其实 var result = xxx 这里就相当于一个回调,但是并不是回调的形式,在这里执行 doLotsOfThingsCanSuspend() 时,函数会被挂起,等待结果返回,这不是一个阻塞的过程,异步线程的切换在挂起函数内进行,因此我们可以像写同步代码一样,书写异步代码,结果返回之后,后面的代码其实相当于回调里面的代码,此时才会继续执行,和回调一样,它的运行环境取决于挂起函数中调用 resume() 方法时的环境。
1  | callCoroutine {  | 
Kotlinx 官方协程扩展框架
添加依赖
1  | compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.18'  | 
借助 launch() 函数来开启一个协程,CommonPool 是内置的一个上下文,他将会在子线程执行任务。
1  | fun test1() {  | 
输出结果为
1  | E/tag: main 开始协程  | 
结果就好像我们在子线程执行了一个任务,他并没有干预外面的代码运行,也没有获得预期的值,我们希望后面的任务在子线程的代码计算完成之后在执行,需要使用 join() 函数。
launch() 函数返回一个 Job 对象,同时 join() 函数需要在一个可挂起的函数内执行,使用 runBlocking() 函数使用 EmptyCoroutineContext 可以在当前位置开启协程;
这样就得到了预期的结果,同样我们可以调用 job.cancel() 来结束这个任务
1  | fun test2() {  | 
输出结果为
1  | E/tag: main 开始协程  | 
获取返回值,上面我们使用了变量的形式来存储计算的值,当我们需要一个任务的返回值时,最好还是让任务能够返回执行完的结果,使用 async() 函数可以返回一个 Deferred 对象,从里面可以取到执行结果
1  | launch(CommonPool) {  | 
输出结果
1  | E/tag: ForkJoinPool.commonPool-worker-1 result,100  | 
在子线程计算,在主线程更新,UI 是一个协程上下文,他使用 Handler 将结果分发到主线程。
1  | launch(UI) {  |