Kotlin-协程学习小计(1)

188 阅读6分钟

协程是什么?

我理解的协程在安卓中就是一个新的线程框架,方便与我们做一些异步操作。它与线程的最大区别就是,他允许我们用看似同步的方式
来写异步代码。比如说我们可以以上下两行代码的形式去进行线程切换,然后再切回来,而不必像线程一样使用回调的形式去操作获取
异步结果。这种方式的优点是:
1,避免了回调地狱 
2,在特定场景可以同时发起多个网络请求,并且合并起来,而不必等待一个请求结果回来后再发起另一个这种现象,
节省了网络请求等待时间

如何启动一个协程

1. runBlocking:T 启动协程

runBlocking:T kotlin顶层函数。他的作用是启动一个新协程,并阻塞调用他的线程,直到协程里面的代码执行完毕。他的返回值是泛型T,就是你协程体中最后一行是什么类型,最终返回的是什么类型T就是什么类型。

例子:

fun main(args: Array<String>) {
    println("Hello World!")
    runBlocking {
        println("1")
        delay(200)
        println("2")
    }
    println("Hello World!2")
}

结果

Hello World!
1
2
Hello World!2

解释:此处的main函数在运行到runBlocking协程时,便阻塞在此,直到runBlocking执行完毕后,再继续执行main函数接下来的方法

runBlocking它的设计目的是将常规的阻塞代码连接到一起,主要用于main函数和测试中。

2. launch:Job启动协程

launch:Job 是CoroutineScope的一个扩展函数(由于CoroutineScope是接口类型,所以launch必须由他的具体子类才可调用,具体看下面例子。)。使用它启动一个协程不会阻塞调用它的线程,必须要在协程作用域(CoroutineScope)中才能调用返回值是一个Job。

例子:(失败

//这样写是不对的,此时launch被编译器标红
CoroutineScope.launch { 
    
}

例子:(成功

//此处GlobalScope是CoroutineScope的具体子类,他可以调用
GlobalScope.launch {

}

例子:

fun main(args: Array<String>) {
    println("hello 1")
    GlobalScope.launch {
        println("launch1")
        delay(200)
        println("launch2")
    }
    println("hello 2")
}

结果:

hello 1
hello 2

Process finished with exit code 0

此时,由于launch没有阻塞调用它的线程,导致launch内的代码并没有打印出结果程序就执行完毕。

3. async:Deferred 启动协程

async:Deferred<T> 启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用。以Deferred对象的形式返回协程任务。返回值泛型TrunBlocking类似都是协程体最后一行的类型。 例子:

fun main(args: Array<String>) {
    println("hello 1")
    GlobalScope.async {
        println("launch1")
        delay(200)
        println("launch2")
    }
    println("hello 2")
}

结果:

hello 1
hello 2

Process finished with exit code 0

上面我们看了协程的几种启动方式,其中launch和async都是必须在协程作用域(CoroutineScope)中才能调用,并且他们的返回值是Job和Deferred。那么什么是协程的作用域,Job和Deferred又分别有什么作用呢?

Job

Job是我们在执行协程任务CoroutineScope.launch方法时生成的,它运行一个指定的代码块,并在该代码块完成时完成。我们可以通过isActiveisCompletedisCancelled来获取到Job的当前状态。Job的状态如下图所示,摘自官方文档:

协程的生命周期图

StateisActiveisCompletedisCancelled
New (optional initial state)falsefalsefalse
Active (default initial state)truefalsefalse
Completing (transient state)truefalsefalse
Cancelling (transient state)falsefalsetrue
Cancelled (final state)falsetruetrue
Completed (final state)falsetruefalse

Defferred

Deferred继承自Job,我们可以把它看做一个带有返回值的Job,下面是Defered类定义:

public interface Deferred<out T> : Job { 
    //返回结果值,或者如果延迟被取消,则抛出相应的异常 
    public suspend fun await(): T 
    public val onAwait: SelectClause1<T> 
    public fun getCompleted():T 
    public fun getCompletionExceptionOrNull(): Throwable? 
}

此类中我们只要调用await()方法就能拿到协程执行的返回值了 例子:

suspend fun main(args: Array<String>) {
    println("hello 1")
    var defered = GlobalScope.async {
        println("launch1")
        delay(200)
        println("launch2")
        50
    }
    var resualt=defered.await();
    println("get defered resualt = $resualt")
}

结果:

hello 1
launch1
launch2
get defered resualt = 50

Process finished with exit code 0

什么是协程作用域

协程作用域CoroutineScope是协程代码的执行范围。我们通常使用CoroutineScope的launch和async扩展方法来启动一个新的作用域。具体理解一个协程作用域请看下面的方法。

private fun test(){ // 作用域开始
    val money = 100; 
    println(money) 
} // 作用域结束
//此时打印会报错,因为money是局部变量,无法在此test方法作用域外调用。
println(money)

协程作用域就像是我们在上面代码中定义的方法。我们所有的协程代码都需要写在协程作用域内部,确保里面的协程都有一个作用域的限制。以能够控制协程代码的取消等操作。

---------------------------------------------(二中有更深入的探讨)

一个简单的协程程序

我们知道到了如何启动一个协程,以及他们的返回值Job和Defered还有协程作用域。那么我们接下来看个最简单的协程示例

fun main(args: Array<String>) {

    runBlocking {
        println("runBlocking---启动一个协程")
    }
    GlobalScope.launch{
        println("launch---启动一个协程")
    }
    GlobalScope.async{
        println("async---启动一个协程")
    }
}

执行结果:

runBlocking---启动一个协程
launch---启动一个协程
async---启动一个协程

Process finished with exit code 0

上面介绍到了协程的返回值,那么我们接下来定义一下变量,来接收一下他们的返回值。

fun main(args: Array<String>) {
    val launchJob = GlobalScope.launch{
        println("launch---启动一个协程")
    }
    println("launchJob---$launchJob")
    val runBlockingJob = runBlocking {
        println("runBlocking---启动一个协程")
        delay(100)
        50
    }
    println("runBlockingJob---$runBlockingJob")
    val asyncJob = GlobalScope.async{
        println("async---启动一个协程")
        "我是返回值"
    }
    println("asyncJob---$asyncJob")
}

上面的代码,我们运行多次得到如下结果:

launchJob---StandaloneCoroutine{Active}@5b80350b
launch---启动一个协程
runBlocking---启动一个协程
runBlockingJob---50
asyncJob---DeferredCoroutine{Active}@1edf1c96
async---启动一个协程

Process finished with exit code 0

此时我们可以得到runBlocking是优先执行的,main方法总是在runBlocking协程内代码执行完毕后再继续往下执行哪怕它暂停了100ms,仍然是它先执行。

下面我们来研究一下launch和async内协程代码和main方法内代码的执行顺序问题。

正常执行(协程内不加等待延迟)

fun main(args: Array<String>) {
    val launchJob = GlobalScope.launch{
        println("launch---启动一个协程")
    }
    println("launchJob---$launchJob")
    val asyncJob = GlobalScope.async{
        println("async---启动一个协程")
        "我是返回值"
    }
    println("asyncJob---$asyncJob")
}

执行多次结果:

launchJob---StandaloneCoroutine{Active}@5b80350b
launch---启动一个协程
asyncJob---DeferredCoroutine{Active}@50134894
async---启动一个协程

Process finished with exit code 0

延迟执行(协程内加delay)

fun main(args: Array<String>) {
    val launchJob = GlobalScope.launch{
        delay(40)
        println("launch---启动一个协程")
    }
    println("launchJob---$launchJob")
    val asyncJob = GlobalScope.async{
        delay(20)
        println("async---启动一个协程")
        "我是返回值"
    }
    println("asyncJob---$asyncJob")

    Thread.sleep(50)
}

多次执行结果

launchJob---StandaloneCoroutine{Active}@5b80350b
asyncJob---DeferredCoroutine{Active}@50134894
async---启动一个协程
launch---启动一个协程

Process finished with exit code 0

上述实验launch协程延迟40ms,async协程延迟20ms,看实验结果我们可以得到如下结论,当使用launch和async启动协程时,main方法体内代码执行永远是顺序执行的。launchJob的打印总是在asyncJob前面,同时两个协程代码快中的打印总是哪个先执行完异步代码,哪个先打印,他们是并行的且互不影响

在协程作用域中直接启动协程

代码展示:

fun main(args: Array<String>) {
    GlobalScope.launch{
        val launchJob = launch{
            println("launch---启动一个协程")
        }
        println("launchJob---$launchJob")
        val asyncJob = async{
            println("async---启动一个协程")
            "我是async返回值"
        }
        println("asyncJob.await---:${asyncJob.await()}")
        println("asyncJob---$asyncJob")
    }
    Thread.sleep(30)
}

结果:

launch---启动一个协程
launchJob---StandaloneCoroutine{Active}@6e9f5a18
async---启动一个协程
asyncJob.await---:我是async返回值
asyncJob---DeferredCoroutine{Completed}@4ddd4401

Process finished with exit code 0

上面代码中我们通过GlobalScope.launch启动了一个协程。又在协程体代码块里面通过launch和async又直接启动了两个协程。为什么我们没有在协程体使用GlobalScope.launch启动,而是使用launch直接启动。前面我们提到过调用launch必须要在协程作用域(Coroutine Scope)中才能调用,因为通过runBlockinglaunchasync启动的协程体等同于协程作用域,所以这里我们就可以直接使用launch启动一个协程。

挂起函数

suspend是kotlin协程中的关键字,kotlin中用suspend关键字修饰的函数叫做挂起函数。一个挂起函数必须在协程或者另一个挂起函数中被调用。

---------------------------------------------(二中有更深入的探讨)

协程调度器

协程调度器是协程中一个很重要的概念,协程调度器确定了相关的协程在哪个线程或哪些线程上执行。它可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

调度器代码:

public actual object Dispatchers {
    @JvmStatic
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
    @JvmStatic
    public actual val Main: MainCoroutineDispatcher
        get() = MainDispatcherLoader.dispatcher
    @JvmStatic
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultScheduler.IO
}

预置调度器

  • Default:默认调度器,CPU密集型任务调度器,适合处理后台计算。通常处理一些单纯的计算任务,或者执行时间较短任务。比如:Json的解析,数据计算等
  • IO:IO调度器,,IO密集型任务调度器,适合执行IO相关操作。比如:网络处理,数据库操作,文件操作等
  • Main:UI调度器, 即在主线程上执行,通常用于UI交互,刷新等
  • Unconfined:非受限调度器,又或者称为“无所谓”调度器,不要求协程执行在特定线程上。

比如我们通过launch启动的时候,因为我们没有传入参数,所有实际上它使用的是默认调度器Dispatchers.Default

GlobalScope.launch{
    println("launch---启动一个协程")
}
//等同于
GlobalScope.launch(Dispatchers.Default){
    println("launch---启动一个协程")
}

我们理解了上面的预置几个调度器后在选择调度器切线程的时候就容易解决了。同时安卓官方为了我们使用方便在安卓协程库中已经为我们提供了几个调度器,如:MainScopelifecycleScopeviewModelScope。它们都是使用的Dispatchers.Main,这些后续我们都将会使用到。

根据我们上面使用的方法,我们好像只有在启动协程的时候,才能指定具体使用那个Dispatchers调度器。如果我要是想中途切换线程怎么办,比如:

  • 现在我们需要通过网络请求获取到数据的时候填充到我们的布局当中,但是网络处理在IO线程上,而刷新UI是在主线程上,那我们应该怎么办。

此时官方为我们提供了一个withContext顶级函数,使用withContext函数来改变协程的上下文,而仍然驻留在相同的协程中,同时withContext还携带有一个泛型T返回值。我们可以调用此函数来进行线程的切换操作,而在完成任务后可以主动切回来将结果返回给调用处。

withContext函数大致结构如下:

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
 //......
}

我们可以先使用launch(Dispatchers.Main)启动协程,然后再通过withContext(Dispatchers.IO)调度到IO线程上去做网络请求,把得到的结果返回,这样我们就解决了我们上面的问题了。

GlobalScope.launch(Dispatchers.Main) {
    val result = withContext(Dispatchers.IO) {
        //网络请求...
        "请求结果"
    }
    btn.text = result
}

协程上下文

扑通------这不重要。

协程启动模式

CoroutineStart是一个枚举类,他保存着协程的启动模式,是启动协程时需要传入的第二个参数。协程启动模式有4种:

  • DEFAULT 默认启动模式,我们可以称之为饿汉启动模式,因为协程创建后立即开始调度,虽然是立即调度,但不是立即执行,有可能在执行前被取消。

  • LAZY 懒汉启动模式,启动后并不会有任何调度行为,直到我们需要它执行的时候才会产生调度。也就是说只有我们主动的调用Jobstartjoin或者await等函数时才会开始调度。

  • ATOMIC 一样也是在协程创建后立即开始调度,但是它和DEFAULT模式有一点不一样,通过ATOMIC模式启动的协程执行到第一个挂起点之前是不响应cancel 取消操作的,ATOMIC一定要涉及到协程挂起后cancel 取消操作的时候才有意义。

  • UNDISPATCHED 协程在这种模式下会直接开始在当前线程下执行,直到运行到第一个挂起点。这听起来有点像 ATOMIC,不同之处在于UNDISPATCHED是不经过任何调度器就开始执行的。当然遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器。

关于上面启动模式的例子:

fun main(args: Array<String>) {
    val defaultJob = GlobalScope.launch{
        println("defaultJob---CoroutineStart.DEFAULT")
    }
    defaultJob.cancel()
    val lazyJob = GlobalScope.launch(start = CoroutineStart.LAZY){
        println("lazyJob---CoroutineStart.LAZY")
    }
    val atomicJob = GlobalScope.launch(start = CoroutineStart.ATOMIC){
        println("atomicJob---CoroutineStart.ATOMIC挂起前")
        delay(100)
        println("atomicJob---CoroutineStart.ATOMIC挂起后")
    }
    atomicJob.cancel()
    val undispatchedJob = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED){
        println("undispatchedJob---CoroutineStart.UNDISPATCHED挂起前")
        delay(100)
        println("undispatchedJob---CoroutineStart.UNDISPATCHED挂起后")
    }
    undispatchedJob.cancel()
}

我们每个模式都分别启动一个,DEFAULT模式启动时,我们接着调用了cancel取消协程,ATOMIC模式启动时,我们在里面增加了一个挂起点delay挂起函数,来区分ATOMIC启动时的挂起前后执行情况,同样的UNDISPATCHED模式启动时,我们也调用了cancel取消协程,我们看实际的日志输出情况:

defaultJob---CoroutineStart.DEFAULT
atomicJob---CoroutineStart.ATOMIC挂起前
undispatchedJob---CoroutineStart.UNDISPATCHED挂起前

Process finished with exit code 0

或者

undispatchedJob---CoroutineStart.UNDISPATCHED挂起前
atomicJob---CoroutineStart.ATOMIC挂起前

Process finished with exit code 0

由上面实验结果可得:

  1. defaultJob默认启动模式在创建后立即调度,但是不是立即执行,所以它有可能被cancel掉,导致没有default这条日志输出。
  2. ATOMIC模式启动的时候也接着调用了cancel取消协程,但是因为没有遇到挂起点,所以挂起前的日志输出了,但是挂起后的日志没有输出。
  3. UNDISPATCHED模式启动的时候也接着调用了cancel取消协程,同样的因为没有遇到挂起点所以输出了UNDISPATCHED挂起前,但是因为UNDISPATCHED是立即执行的,所以他的日志UNDISPATCHED挂起前输出在ATOMIC挂起前的前面(注意这里是概率事件,主要突出UNDISPATCHED是立即执行)。
  4. 接着我们在补充一下关于UNDISPATCHED模式。我们上面有提到当以UNDISPATCHED模式启动时,遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器。这句话我们又要怎么理解呢。我们还是以一个例子来认识解释UNDISPATCHED模式,比如:
private fun testUnDispatched(){
    GlobalScope.launch(Dispatchers.Main){
       val job = launch(Dispatchers.IO) {
           Log.d("${Thread.currentThread().name}线程", "-> 挂起前")
           delay(100)
           Log.d("${Thread.currentThread().name}线程", "-> 挂起后")
       }
       Log.d("${Thread.currentThread().name}线程", "-> join前")
       job.join()
       Log.d("${Thread.currentThread().name}线程", "-> join后")
   }
}

那我们将会看到如下输出,挂起前后都在一个worker-1线程里面执行:

D/main线程: -> join前
D/DefaultDispatcher-worker-1线程: -> 挂起前
D/DefaultDispatcher-worker-1线程: -> 挂起后
D/main线程: -> join

现在我们在稍作修改,我们在子协程launch的时候使用UNDISPATCHED模式启动:

 private fun testUnDispatched(){
     GlobalScope.launch(Dispatchers.Main){
        val job = launch(Dispatchers.IO,start = CoroutineStart.UNDISPATCHED) {
            Log.d("${Thread.currentThread().name}线程", "-> 挂起前")
            delay(100)
            Log.d("${Thread.currentThread().name}线程", "-> 挂起后")
        }
        Log.d("${Thread.currentThread().name}线程", "-> join前")
        job.join()
        Log.d("${Thread.currentThread().name}线程", "-> join后")
    }
 }

那我们将会看到如下输出:

D/main线程: -> 挂起前
D/main线程: -> join前
D/DefaultDispatcher-worker-1线程: -> 挂起后
D/main线程: -> join

我们看到当以UNDISPATCHED模式即使我们指定了协程调度器Dispatchers.IO挂起前还是在main线程里执行,但是挂起后是在worker-1线程里面执行,这是因为当以UNDISPATCHED启动时,协程在这种模式下会直接开始在当前线程下执行,直到第一个挂起点。遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器,即join处恢复执行时,因为所在的协程有调度器,所以后面的执行将会在调度器对应的线程上执行。

我们再改一下,把子协程在launch的时候使用UNDISPATCHED模式启动,去掉Dispatchers.IO调度器,那又会出现什么情况呢:

 private fun testUnDispatched(){
     GlobalScope.launch(Dispatchers.Main){
        val job = launch(start = CoroutineStart.UNDISPATCHED) {
            Log.d("${Thread.currentThread().name}线程", "-> 挂起前")
            delay(100)
            Log.d("${Thread.currentThread().name}线程", "-> 挂起后")
        }
        Log.d("${Thread.currentThread().name}线程", "-> join前")
        job.join()
        Log.d("${Thread.currentThread().name}线程", "-> join后")
    }
 }
D/main线程: -> 挂起前
D/main线程: -> join前
D/main线程: -> 挂起后
D/main线程: -> join

我们发现它们都在一个线程里面执行了。这是因为当通过UNDISPATCHED启动后遇到挂起,join处恢复执行时,如果所在的协程没有指定调度器,那么就会在join处恢复执行的线程里执行,即挂起后是在父协程(Dispatchers.Main线程里面执行,而最后join后这条日志的输出调度取决于这个最外层的协程的调度规则。

现在我们可以总结一下,当以UNDISPATCHED启动时:

  • 无论我们是否指定协程调度器,挂起前的执行都是在当前线程下执行。
  • 如果所在的协程没有指定调度器,那么就会在join处恢复执行的线程里执行,即我们上述案例中的挂起后的执行是在main线程中执行。
  • 当我们指定了协程调度器时,遇到挂起点之后的执行将取决于挂起点本身的逻辑和协程上下文中的调度器。即join处恢复执行时,因为所在的协程有调度器,所以后面的执行将会在调度器对应的线程上执行。

同样的我们点到为止,关于启动模式的的相关内容我们就现讲到这里。

文章大量参考这个文章,加上自己的理解。勿喷史上最详Android版kotlin协程入门进阶实战