kotlin-----协程

什么是协程

对于android中kotlin的协程来说,协程就是由kotlin 官方提供的一套线程切换的API,和线程池的功能是一样的,只不过他的优势在于用同步的方式写出了异步的代码。因为协程是运行在jvm上的,jvm没有协程这个概念。

kotlin的协程如果是运行在当前的线程,相当于往当前的线程的handler队列中,post了一个任务,也会造成阻塞,记住如果协程没有切换线程的话,那个有可能也会造成阻塞。这一点我待会会验证

好,现在开始从使用

协程如果使用

在kotlin中协程并没有被纳入标准库,所以使用的时候还是得引入:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"

第二个库是android环境使用的。

开启协程的两个方法:

  launch {  }
             
   async {  }

我们看下这两个方法

launch

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

async

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

发现这两个方法都是 CoroutineScope(协程作用域)类的扩展函数,所以说这两个函数必须得在用协程作用域对象才能调用。

区别在于,lanch不会返回运行结果,而async返回的Deferred对象,通过Deferred对象的await()

方法可以获得协程的执行结果。

还有一个

withContext(Dispatchers.Main){
    
}

他其实是

async {

}.await()

的简写,所以会阻塞协程,因为await()方法是挂起函数

解释下几个名词:

CoroutineScope:协程作用域

public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}

协程作用域中就一个属性,就是协程上下文,说明协程coroutineScope是对CoroutineContext的一个封装。 是指一个协程的运行范围(或者说生命周期)用来让用户继承,来实现取消协程和停止操作。自己可以控制

 CoroutineContext:

协程的上下文可以理解为协程运行的配置信息,执行协程需要的那个配置,比如:协程的名字,协程运行在那个线程上。

我看看下coroutineContext类,其实就相当于一个容器,里面存放这各种属性,类似于hash那种存储。

public interface CoroutineContext {

    //重写了+号运算符
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

    public interface Key<E : Element>

        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }
}

所以他可以这样用

我们看launch函数,可以传一个coroutineContext

 我们可以这样使用

launch(Dispatchers.Main+CoroutineName("协程名称")) {  }

CoroutineDispatcher  协程调度器

kotlin给我们提供了四种调度器

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

Default:默认调度器,CPU密集型任务调度器,通常处理一些单纯的计算任务,或者执行时间较短任务。例如数据计算
IO:IO调度器,IO密集型任务调度器,适合执行IO相关操作。比如:网络请求,数据库操作,文件操作等
Main:UI调度器,只有在UI编程平台上有意义,用于更新UI,例如Android中的主线程
Unconfined:非受限调度器,无所谓调度器,当前协程可以运行在任意线程上

CoroutineStart:协程的启动模式

public enum class CoroutineStart {
DEFAULT,
LAZY,
ATOMIC,
UNDISPATCHED;
}

可以看到CoroutineStart是一个枚举类,有四种类型。
DEFAULT默认启动模式,协程创建后立即开始调度,注意是立即调度而不是立即执行,可能在执行前被取消掉。
LAZY懒汉启动模式,创建后不会有任何调度行为,直到我们需要它执行的时候才会产生调度。需要我们手动的调用Job的start、join或者await等函数时才会开始调度。
ATOMIC 在协程创建后立即开始调度,但它和DEFAULT模式是有区别的,该模式下协程启动以后需要执行到第一个挂起点才会响应cancel操作。
UNDISPATCHED协程在这种模式下会直接开始在当前线程下执行,直到运行到第一个挂起点。和ATOMIC很像,但UNDISPATCHED很受调度器的影响

Job

launch()方法返回的是一个job对象,这个对象可以检测协程的生命周期,也可以取消协程的执行
public interface Job : CoroutineContext.Element {
  
    public companion object Key : CoroutineContext.Key<Job>

    //判断协程是否是活的
    public val isActive: Boolean

    
   //判断协程是否完成
    public val isCompleted: Boolean

   //判断协程是否取消
    public val isCancelled: Boolean



    public fun getCancellationException(): CancellationException

    //开始协程
    public fun start(): Boolean


    //取消协程
    public fun cancel(cause: CancellationException? = null)


   

Deferred  实际上是继承job

所以也有对协程进行操作的方法,其中还提供了 一个重要的方法,await()方法,获得协程返回的值,await是一个挂起函数,所以执行到这里的时候会阻塞当前的协程。

public interface Deferred<out T> : Job {

    public suspend fun await(): T

suspend 中文 暂停的意思

suspend函数会阻塞当前的协程,但是不会阻塞开启协程的线程

在函数的前面申明一个suspend的时候,这个函数就变成了挂起函数,挂起函数只能在挂起函数中调用,但是这个只是起到一个提醒的作用,真正起到挂起作用的是函数中的代码。所以说如果你申明一个挂起函数,但是里面没有实际的挂起操作,编译器会给你置灰,意思是告诉你这个函数没必要申明。那么要这个申明有什么用呢?用于创建者给调用者一个提醒,告诉调用者说这个函数是一个挂起函数,可能是一个耗时的函数

挂起函数作用:

协程运行到挂起函数的时候,会暂停到这,切出去运行挂起的函数,运行完挂起函数后,再切回来重新运行,其实就是一个callback+状态机的机制

 如果里面加上一个系统提供的真正的挂起函数就没事了

父协程和子协程的关系

1.子协程中的协程上下文的信息会copy父协程,除非自己修改

2.父协程结束后,他里面的子协程都会结束

证明第一点:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
         GlobalScope.launch(Dispatchers.Main+CoroutineName("父协程名字")) {
             Log.e("---","协程上下文-1=${this.coroutineContext}")
             launch {
                 Log.e("---","协程上下文-2=${this.coroutineContext}")
             }
             withContext(Dispatchers.IO+CoroutineName("子协程")){
                 Log.e("---","协程上下文-2=${this.coroutineContext}")
             }
         }
    }

打印的结果:

E/---: 协程上下文-1=[CoroutineName(父协程名字), StandaloneCoroutine{Active}@d2a3906, Dispatchers.Main]
E/---: 协程上下文-3=[CoroutineName(子协程), DispatchedCoroutine{Active}@8fe4bc7, Dispatchers.IO]
E/---: 协程上下文-2=[CoroutineName(父协程名字), StandaloneCoroutine{Active}@24715f4, Dispatchers.Main]

 我们发现协程协程2和父协程的协程上下文的配置信息是一样的,而协程3不一样,说明子协程会继承父协程的协程配置信息

 CoroutineExceptionHandler 协程异常处理

我们在写代码的时候,肯定会遇到异常的情况,正常我们处理异常使用try …catch捕获,但难免会出现遗漏。CoroutineExceptionHandler 就是协程专门捕获异常的类,协程中出现的异常都会被捕获并由CoroutineExceptionHandler的handleException方法返回给我们进行处理。

public interface CoroutineExceptionHandler : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>

    public fun handleException(context: CoroutineContext, exception: Throwable)
}

handleException会返回两个参数,第一个参数是出现异常的协程,第二个参数是出现的异常。
示例如下:我们手动抛出一个NullPointerException异常,然后创建一个CoroutineExceptionHandler赋给协程。

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    Log.e("捕获异常", "${coroutineContext[CoroutineName]} :$throwable")
}

GlobalScope.launch(Dispatchers.Main + CoroutineName("主协程")+exceptionHandler) {
    Log.e("协程的coroutineContext",this.coroutineContext.toString())
    throw NullPointerException()
}
打印结果:
协程的coroutineContext: [CoroutineName(主协程), com.jf.simple.ThirdActivity$onCreate$$inlined$CoroutineExceptionHandler$1@288ff9, StandaloneCoroutine{Active}@b95b3e, Dispatchers.Main]

捕获异常: CoroutineName(主协程) :java.lang.NullPointerException


 

问题解析

1.如果运行的协程就是运行在当前的开启协程的线程是相当于post一个任务到线程的handler任务栈吗?

测试代码:

    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
         GlobalScope.launch(Dispatchers.Main+CoroutineName("父协程名字")) {
              Log.e("---","111")
         }
        Log.e("---","222")
    }

打印结果:

                com.example.mytest2                  E  222
                com.example.mytest2                  E  111

打印结果 222却在111的前面,说明确实是post了一个任务

如何创建一个协程作用域

我们知道开启协程的方法有两种,其实还有一种 runBlocking,但是这种开启的协程会阻塞当前的线程,只有当协程结束后,开启协程的线程才会结束,会造成内存泄露,一般项目中不使用。

lanch()

async()

他两属于CoroutineScope的扩展函数,所以必须的有CoroutineScope的对象才能调用

方法1:

GlobalScope 就是一个协程作用域的单例,所以可以直接使用
@DelicateCoroutinesApi
public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}
GlobalScope.launch() {
}

方法2:

class MainActivity : AppCompatActivity() {
    lateinit var coroutine:CoroutineScope

    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
       coroutine=  CoroutineScope(Dispatchers.IO)  //创建一个协程作用域
        coroutine.launch { 
            Log.e("---","开始了协程")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        coroutine.cancel() //协程取消
    }

}

但是这里有一个问题,假如我需要开启很多协程作用域,我是不是得调用很多cancel

如下所示:

package com.example.mytest2

import android.annotation.SuppressLint
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.Settings.Global
import android.util.Log
import android.view.View
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.math.log
import kotlin.system.measureTimeMillis
import kotlin.time.measureTimedValue

class MainActivity : AppCompatActivity() {
    lateinit var coroutine:CoroutineScope
    lateinit var coroutine1:CoroutineScope

    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
       coroutine=  CoroutineScope(Dispatchers.IO)
        coroutine.launch {
            Log.e("---","开始了协程")
        }

        coroutine1=  CoroutineScope(Dispatchers.IO)
        coroutine1.launch {
            Log.e("---","开始了协程")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        coroutine.cancel()
        coroutine1.cancel()
    }

}

优化:我们知道job是管理协程的,那么CoroutineScope是如何管理协程的呢?

public fun CoroutineScope.cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
    job.cancel(cause)
}

发现他其实也是调用了job的cancel,所以我们创建一个job,让一个job对象管理多个协程的取消



class MainActivity : AppCompatActivity() {
    val job=Job()
    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
      
        CoroutineScope(Dispatchers.IO+job).launch {
            Log.e("---","开始了协程")
        }

         CoroutineScope(Dispatchers.IO+job).launch {
            Log.e("---","开始了协程")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }

}

 要注意上面的

CoroutineScope(Dispatchers.IO+job)是一个方法,不是创建一个对象,而是一个方法返回一个对象
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

方法3

实现CoroutineScope 接口,因为CoroutineScope 接口中有一个未实现的属性,所以继承者得初始化这个属性,我们可以采用委托类的方式实现

public interface CoroutineScope {

    public val coroutineContext: CoroutineContext
}
实现CoroutineScope 的接口,协程的上下文让MainScope()方法创建的类实现实现
class MainActivity : AppCompatActivity() ,CoroutineScope by MainScope() {
    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        launch {   //这个协程是运行在主线程的,因为mainScope创建的协程上下文中的线程是主线程
            
        }
    }
    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}

我们看下MainScope()方法,

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
注意这个方法创建的协程上下文的指定线程是主线程,所以说如果子协程不指定线程的话,子协程也是在主线程中运行

方式4:

如果我们自己去管理协程的话,感觉麻烦,有时候又会忘记取消,容易造成内存泄露,所以android为我们提供了一些库

//第四种: android提供的和lifecycle相关的库
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
lifecycleScope.launch {  } 用在activity和fragment中

viewModelScope.launch{} 用在viewModel中
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
官网地址

协程的原理解析

协程的非阻塞原理其实就是利用 callback + 状态机机制实现的

借鉴文章:

kotlin语法进阶 - 协程(二)挂起函数原理_飞过那时的城镇的博客-CSDN博客

suspend :暂停的意思
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_coroutine)
        val launch = GlobalScope.launch(Dispatchers.Main) {   //实质上相当于往主线程中post了一个新任务,这个任务就是{}闭包的内容

            println("thread name= ${Thread.currentThread().name}")
            println("======12")
            delay(500)
            println("======23")
        }
        println("--end--")
    }


我们看下launch函数:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit   //执行的也是也是我们传入好的闭包,只是给它改成了挂起函数
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

分析这个代码:

class CoroutineActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_coroutine)

        
        运行到挂起函数的时候,相当于线程执行到这里切断了,一部分去执行挂起函数,一部分去按顺序去执行
        val launch = GlobalScope.launch(Dispatchers.Main) {  //挂起函数
            println("thread name= ${Thread.currentThread().name}")
            println("======12")
            delay(500)
            println("======23")
        }
        println("--end--")  //接下来的内容
    }
}

要注意的是:suspend 关键字只是一个提示的作用,不能起到挂起的作用,有挂起作用的方法有withcontent(),launch等

【码上开学】Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了_哔哩哔哩_bilibili码上开学 ( kaixue.io ) 之:Kotlin 的协程第 2 期。看完有啥想法,来留言讨论啊!https://www.bilibili.com/video/BV1KJ41137E9?spm_id_from=333.999.0.0

猜你喜欢

转载自blog.csdn.net/xueyoubangbang/article/details/123095882