Kotlin协程的使用

挂起协程

《Kotlin 协程概览》 一文中,我们知道Kotlin协程可以被挂起从而让出线程资源。那么协程在什么情况下会被挂起?我们如何能够操作挂起一个协程?

在《Kotlin 协程概览》一文中我们知道在协程中调用 delay 函数可以将协程挂起,并且在一定时间后再次回到协程执行后续的代码。

GlobalScope.launch {
    delay(1000L)
    println("After delay")
}

在实际开发场景中,我们很少会使用 delay 函数,我们使用协程是希望能够用同步的方式写出异步的代码,避免使用回调函数。例如:我们当前在UI线程,现在需要去做一个网络请求下载图片,等待图片下载成功后我们需要将图片展示出来,面对这个场景,我们可以使用协程写出类似如下的代码:

GlobalScope.launch {
    val image = downloadImage()
    showImage(image)
}

fun downloadImage() : Image {
    //...
}

fun showImage(image: Image) {
    //...
}

上述代码中,我们使用GlobalScope发起了一个协程,则该协程的执行线程默认会使用Dispathcers.Default来分发,因此协程首先会绑定到线程池中的一个异步线程去执行,downloadImage函数会在这个异步线程去执行下载图片,下载完成后会调用showImage函数去显示刚才下载回来的图片。
协程内部的代码执行过程是从上而下串行执行的,上述代码实际也是在同一个线程中串行执行的,因此showImage不在UI线程执行会报错,我们需要在执行showImage函数的时候将协程从之前的异步线程挂起,然后再绑定到UI线程去执行。
因此在实际开发的场景中,我们需要挂起一个协程通常是为了将它从一个线程切到另一个线程去执行。

那么我们该如何实现切换协程执行的线程?答案是使用 挂起函数

挂起函数

Kotlin中的挂起函数使用 suspend 关键字修饰,表示这个函数需要在协程中执行,并且在执行之后会自动根据当前协程的上下文将协程切换到其他线程去执行。

suspend fun showImage() {
   //...
}

那么我们给上述示例中的showImage函数加上了suspend关键字来修饰,是否就可以实现下载图片成功之后自动切换到UI线程?
答案是否定的,因为showImage函数实际并没有执行任何切换线程的操作。实际上对于这种单纯使用suspend关键字来修饰的函数,编辑器也会给出提示告警 Redundant 'suspend' modifier
实际上我们只能通过使用Kotlin协程框架提供的挂起函数来实现切换线程,例如 delay 函数、withContext 函数:

public suspend fun delay(time: Long) {
}

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

delay函数和withContext函数都是使用suspend关键字修饰的函数,并且函数实现是调用了Kotlin协程框架内部的其他挂起函数实现了切换线程。

withContext函数接收两个参数,第一个参数是附加的CoroutineContext信息,block 是一段在协程中执行的lambada表达式,并且会有一个返回值。
withContext函数的特点是可以通过第一个参数指定CoroutineDispatcher从而实现将协程切换到指定的线程去执行传入的lambada表达式,并且会在执行结束后再使用协程原始的CoroutineDispatcher来决定将协程切换到哪个线程继续执行。

suspend fun showImage() {
   withContext(Dispatchers.Main) {
  	// show image in the ui thread
   }
   //back to the original Dispatchers.Default
}

上述showImage函数也不再会收到编辑器的警告,事实上,如果我们在一个函数中调用了其他挂起函数,则该函数也必须使用 suspend 关键字来修饰。挂起函数只能在协程中被调用或者是在另一个挂起函数中被调用。

在Android中发起协程

发起一个协程,首先我们需要指定一个CoroutineScope,前述的示例我们都是使用的GlobalScope,但GlobalScope是一个全局的单例。
在Android中Kotlin协程库为我们提供了与Lifecycle结合的CoroutineScope,即 LifecycleCoroutineScope

public abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
    internal abstract val lifecycle: Lifecycle

    public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenCreated(block)
    }

    public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenStarted(block)
    }

    public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenResumed(block)
    }
}

LifecycleCoroutineScope 是一个抽象类,提供了三个发起协程的函数:

  • launchWhenCreated
    当监听的LifecycleOwner至少是CREATED状态的时候才发起协程,并返回相关的Job
  • launchWhenStarted
    当监听的LifecycleOwner至少是STARTED状态的时候才发起协程,并返回相关的Job
  • launchWhenResumed
    当监听的LifecycleOwner至少是RESUMED状态的时候才发起协程,并返回相关的Job

通过LifecycleCoroutineScope发起的协程初始是绑定到UI线程执行的,此外,LifecycleCoroutineScope还可以保证当监听的LifecycleOwner进入DESTROYED状态的时候会自动取消之前发起的协程。
LifecycleCoroutineScope的实现类是LifecycleCoroutineScopeImpl

internal class LifecycleCoroutineScopeImpl (
    override val lifecycle: Lifecycle, 
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {

    fun register() {
        launch(Dispatchers.Main.immediate) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                coroutineContext.cancel()
            }
        }
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        }
    }
}

public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
	//return LifecycleCoroutineScopeImpl()
    }

Kotlin为LifecycleOwner类和Lifecycle类都添加了一个扩展属性 lifecycleScope,在Lifecyle的扩展属性 lifecycleScopeget() 函数中会创建一个LifecycleCoroutineScopeImpl实例。

因此,我们可以在Activity等实现了LifecycleOwner接口的类中很方便地发起一个协程:

class TestActivity : AppCompatActivity() {

    private fun loadImage() {
        lifecycleScope.launchWhenResumed {
            //切到IO线程
            val image = withContext(Dispatchers.IO) {
                loadImage()
            }
           //自动切回UI线程
            showImage(image)
        }
    }

    private fun loadImage() : Image {
    }

    private fun showImage(image: Image) {
    }
}

取消协程

我们可以通过CoroutineContext来取消一个协程的执行,cancel 函数是CoroutineContext的一个扩展函数,实际上是取消协程中执行的Job

public fun CoroutineContext.cancel(cause: CancellationException? = null) {
    this[Job]?.cancel(cause)
}

Kotlin协程实现取消的原理是在一些特定的代码检查点去判断当前协程状态是否已经变成了取消状态,如果是则结束后续协程代码的执行。例如:Kotlin协程库提供的所有挂起函数在将协程从挂起点恢复执行的时候,都会检查当前协程状态是否已经变成取消状态,如果已经是取消状态则会抛出 CancellationException 从而结束整个协程。

val job = launch(Dispatchers.Default) {
    var i = 0
    while (i < 5) {
        delay(1000)
        i++
    }
}
sleep(2000)
job.cancelAndJoin()

当调用 Job 类的cancel方法后,会导致挂起函数 delay 在协程恢复执行时抛出CancellationException,从而实现取消协程的执行。
而假设我们像如下代码所示使用 try-catch 捕获挂起函数 delay 抛出的异常,则协程便无法及时取消了,直到循环迭代结束而自然终止。

val job = launch(Dispatchers.Default) {
    var i = 0
    while (i < 5) {
        try {
            delay(1000)
            i++
        } catch (e: Exception) {
            //
        }
    }
}
sleep(2000)
job.cancelAndJoin()

除了主动调用 Job 的 cancel 方法之外,我们还可以给协程指定一个超时取消的时间,使用挂起函数 withTimeout 即可实现:

val job = launch(Dispatchers.Default) {
    withTimeout(3000) {
        var i = 0
        while (i < 5) {
           delay(1000)
           i++
        }
    }
}

当协程从挂起函数 delay 恢复执行的时候会判断当前协程是超时取消的状态而抛出 TimeoutCancellationException ,从而实现协程的超时自动取消执行。
与 withTimeout 函数类似的还有 withTimeoutOrNull 挂起函数,可以实现超时取消协程的时候返回 null 。

posted @ 2023-09-22 19:22  jqc  阅读(140)  评论(0编辑  收藏  举报