协程 Coroutine

139 阅读4分钟

一、协程简介

协程是一种编码思想,为了帮助我们在处理多线程编码时提供更加友好和简便的编码的方式。在Kotlin中协程可以简单理解为一个线程框架。

二、协程优势

  1. 可以用同步的方式写异步代码

  2. 可以灵活控制协程的执行与结束,不用被动的等待通知

  3. 可以灵活的在不同线程之间切换

一个栗子:

需求:四个接口请求A/B/C/D,C需要获取A/B的执行结果后才能执行,D需要C完成后才能执行

//普通线程:
fun apiA(object: callback(){
    获取 —> 结果A
    调用 —> 接口AB执行完毕检测方法()
} )

fun apiB(object: callback(){
    获取 —> 结果A
    调用 —> 接口AB执行完毕检测方法()
} )

fun 接口AB执行完毕检测方法(){
    if( 结果A!=null && 结果B!=null ){
        apiC(结果A , 结果B, object: callback(){
            调用 —> apiD(结果C)
        }
    }
}

// 开始执行
fun start(){
    apiA();
    apiB();
}

//协程AB同步进行,协程C在AB完成后执行,D在C完成后执行
val 结果A = 协程A执行()
val 结果B = 协程B执行()
val 结果C = 协程C执行(结果A, 结果B)
协程D执行(结果C)

三、导入协程

高版本Android Sutio添加Kotlin时,已自动添加了Kotlin协程支持;

若不能使用,可添加一下依赖:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"

四、协程的几种用法

1、runBlocking

「不推荐」会阻塞线程

runBlocking{
    getUserInfo(uid)
}

2、GlobalScope.launch

「慎用」 此方法虽然不会阻塞线程,但是它的生命周期与app一致,并且不能取消

GlobalScope.launch{
    getUserInfo(uid)
}

3、自行创建CoroutineScope

「推荐」可以灵活的控制协程的生命周期

//此处的context是CoroutineContext,和Activity继承的Context不是同一个东西
val coroutineScope = CoroutineScope(context)

coroutineScope.launch{
    getUserInfo(uid)
}

四、launch

/**
 *launch 源码
 *launch 是CoroutineScope的扩展函数
 *并返回一个新的协程(Coroutine)
 */
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
}
//用法
//切换到主线程
coroutineScope.launch(Dispatchers.Main){...}

//切换到IO线程执行
coroutineScope.launch(Dispatchers.IO){...}

//小栗子
coroutineScope.launch(Dispatchers.IO){
    val userInfo = getUserInfo()
}

通过指定Dispatchers来切换不同的线程;

如果不指定,则协程默认在其所处方法的线程中执行任务。

//同步方式写异步代码 「可以用withContext替换」
coroutineScope.launch(Dispatchers.Main) {
    tv_name.text = "xxx"

    // 切换到IO线程
    launch(Dispatchers.IO){

        // 获取用户信息
        val userInfo = getUserInfo()

        // 切换到UI线程
        launch(Dispatchers.Main){

            // 修改用户名显示
            tv_name.text = userInfo.username

            // 切换到IO线程
            launch(Dispatchers.IO) {
                // 获取消息列表
                val msgList = getMessageList(userInfo.token)

                // 切换到UI线程
                launch(Dispatchers.Main){
                    // 显示消息列表
                }
            }
        }
    }
}

// getUserInfo getHomeInfo 并发执行
coroutineScope.launch(Dispatchers.Main) {

    // 切换到IO线程
    launch(Dispatchers.IO){
        // 获取用户信息
        val userInfo = getUserInfo()
    }

    // 切换到IO线程
    launch(Dispatchers.IO){
        // 获取首页信息
        val userInfo = getHomeInfo()
    }
}

五、withContext

1、消除launch嵌套,解决多个launch嵌套不美观问题

2、withContext只能在CoroutineScopre内部使用

coroutineScope.launch(Dispatchers.Main) {
    tv_name.text = "xxx"

    // 获取用户信息
    val userInfo = withContext(Dispatchers.Main) {
        getUserInfo()
    }

    tv_name.text = userInfo.username

    // 获取消息列表
    val msgList = withContext(Dispatchers.IO) {
        getMessageList(userInfo!!.token)
    }

    // 显示消息列表
}

六、async/await

1、launch 和 withContext 不适合用于需要返回结果的并发场景

2、「async」 只是创建,「await」 才是执行

3、async 也可以指定运行线程

val a = async(Dispatchers.IO){getUserInfo()}
/**
 *asyne源码
 *返回Deferred(Deferred是Job的子类)
 */
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.launch(Dispatchers.IO) {
    //这里并没有真正执行getUserInfo(),只是返回了一个名为a的Deferred
    val a = async{ getUserInfo() }

    //这里真正开始执行协程

    //await() 返回的数据类型取决于async{}最后一行代码返回的类型
    val userInfo = a.await()
}
并发

栗子一:

// getUserInfo()先执行,getMessage在getUserInfo执行完成后再执行。是顺序执行
coroutineScope.launch(Dispatchers.IO){
    val a1 = async{ getUserInfo() }
    val userInfo = a1.await()
    val a2 = async{ getMessage(userInfo.token) }
    val msgList = a2.await()
}

栗子二:

/**
 *getUserInfo() getHomeInfo() 是并发执行的
 *「await特性」:
 * 当有await()执行时,它不仅只执行自己对于的async,它会使得前面所有未执行 的async都开始执行
 */
coroutineScope.launch(Dispatchers.IO){
    val a1 = async{ getUserInfo() }
    val a2 = async{ getHomeInfo() }
    val userInfo = a1.await()
    val msgList = a2.await()
}

七、suspend

1、将函数挂起、暂停;在协程中主要用于修饰函数

2、流程:

  • 协程在执行中遇到了被suspend标记的函数;
  • 这个函数将被切换到别的线程上去执行;
  • 此时的代码执行权在这个被切换出去的函数上,因此位于这个函数后面的协程代码将不会得到执行;
  • 当这个函数执行完毕后,协程会切换回到原来的线程,代码执行权回归原线程,在这个函数后面的协程代码会被执行。
fun init() {
    coroutineScope.launch {
        val userInfo = getUserInfo()
        tv_name.text = userInfo.name
    }
}

// 请求用户信息
suspend fun getUserInfo(): UserInfo {
    return withContext(Dispatchers.IO){
        ...
    }
}

withContext等很多方法都带有suspend

//withContext源码
public suspend fun <T> withContext(
        context: CoroutineContext,
        block: suspend CoroutineScope.() -> T
    ): T = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
    ...
}

「注意」

1、suspend只是作为一个标记,提醒协程去做出相应处理,真正的切换线程等操作都是协程自身完成,不是被suspend修饰的函数自身能力;
2、suspend修饰的函数可以被其他持有suspend的函数调用,但是最终这些suspend的调用执行职能在协程中进行,不能像其他方法那样单独使用。