• 作者:老汪软件技巧
  • 发表时间:2024-11-08 07:01
  • 浏览量:

在 kotlin 中,我们不可避免地会使用到协程,这篇文章将介绍一些协程中需要注意的要点。

屏幕截图 2024-10-31 211631.png

协程的 cancel 不能中断线程

如果我们在协程中使用了线程的并发集合,比如LinkedBlockingQueue,ArrayBlockingQueue等等,这时候我们调用了它的阻塞接口,此时我们无法使用协程的 cancel来中断当前线程。这是因为在 Java 多线程中,我们需要使用interrupt方法才能中断线程,而 cancel 是不行的。kotlin 官方考虑到了这个问题,它提供了方便的 runInterruptible 方法。代码示例如下:

class Test  {
    suspend fun test() {
        runInterruptible { // 内部会调用 interrupt 方法
            while (true){
                Thread.sleep(100)
                println("do something")
            }
        }
    }
}
fun main(): Unit = runBlocking {
    val t = Test()
    val job = launch(Dispatchers.IO) {
        t.test()
    }
    delay(500)
    job.cancel() // 这里只需要 cancel 就可以了
    delay(1000)
    println("end")
}

Job 的 cancel 才会取消协程

如下代码所示,我们创建一个 CoroutineContext 来启动一个协程,然后等待一段时间后 cancel 协程。执行后,我们可以发现 cancel 是没有效果的。

val threadContext = newSingleThreadContext("newThread")
CoroutineScope(threadContext).launch {
    while (true) {
        println("do something")
        delay(500)
    }
}
handler.postDelayed({
    threadContext.cancel() // 没有效果
    println("threadContext cancel")
}, 5000)

这是因为虽然我们创建的 CoroutineContext 有 cancel 方法,但是其内部实现是通过获取 CoroutineContext 的 Job 来 cancel 的。由于我们没有添加 Job,导致获取不到对应的 Job 对象,因此 cancel 没有执行。

/**
 * Cancels [Job] of this context with an optional cancellation cause.
 * See [Job.cancel] for details.
 */
public fun CoroutineContext.cancel(cause: CancellationException? = null) {
    this[Job]?.cancel(cause)
}

如果你对 CoroutineContext 还不熟悉,可以看看 这篇文章。在文章中我们介绍过,CoroutineContext 的功能类似一个 Map,内部包含多个元素;而每一个元素都是 CoroutineContext 的实现,结构如下图所示:

创建新 Job 时注意不要破坏协程的结构化

当我们同时使用 CoroutineScope 和设置新的 CoroutineContext(包含Job) 时,哪个才是有效的?答案是后者,代码示例如下:

val newContext = threadContext + Job()
lifecycleScope.launch(newContext) {
    while (true) {
        println("do something")
        delay(500)
    }
}
handle.postDelayed({
    lifecycleScope.cancel() // 无效
    println("lifecycleScope cancel")
}, 1000)
handler.postDelayed({
    newContext.cancel() // 有效
    println("threadContext cancel")
}, 5000)

这是因为我们设置新的 Job 破坏了之前的结构。之前的结构是 lifecycleScope 的 Job 为 parent;而现在 新创建的 Job 为 parent。因此在设置新的 CoroutineContext 时需要注意,是否会影响到之前协程的结构。

挂起函数应该考虑协程取消

和 Java 的 interrupt 方法类似,kotlin 中的挂起函数默认是不会主动处理协程取消的。这需要你主动调用 ensureActive 方法,或者判断 isActive。

suspend fun testCancel() {
    while (true) {
        coroutineContext.ensureActive()
        println("do something")
    }
}
suspend fun testCancel() {
    while (coroutineContext.isActive) {
        println("do something")
    }
}

协程中try-catch异常时考虑 CancellationExceptions

当我们调用 cancel 方法来取消协程时,其内部是通过抛出 CancellationExceptions 的异常来实现的。如下代码所示,如果我们使用 runCatching 或者 try-catch(e: Exception) 包裹协程方法时,如果此时取消协程,不是直接退出,而是继续执行。

lifecycleScope.launch(Dispatchers.IO) {
    runCatching {
        // do something
        while (true) {
            delay(500)
            println("do something1")
        }
    }.onFailure {
        println(it)
    }
    while (true) { // 执行这里,无法退出
        println("do something2")
    }
}

不要在子协程中设置 CoroutineExceptionHandler

使用 CoroutineExceptionHandler 处理复杂结构的协程异常时,它仅在顶层协程中起作用。

val handle = CoroutineExceptionHandler { coroutineContext, throwable ->
    println("处理异常")
}
GlobalScope.launch(Dispatchers.IO) {
    launch {
        delay(200)
        println("1")
    }
    launch(handle) { // 无效
        delay(100)
        throw NullPointerException()
    }
}

正确代码示例如下:

val handle = CoroutineExceptionHandler { coroutineContext, throwable ->
    println("处理异常")
}
GlobalScope.launch(Dispatchers.IO + handle) { // 放在顶层协程中才能起作用
    launch {
        delay(200)
        println("1")
    }
    launch() {
        delay(100)
        throw NullPointerException()
    }
}

使用 repeatOnLifecycle 而不是 launchWhenXXX

在 Android 中,推荐使用 repeatOnLifecycle 而不是 launchWhenXXX 来处理不同不同生命周期的事件。

lifecycleScope.launchWhenCreated {
    // do something
}
lifecycleScope.launch(Dispatchers.IO) {
    lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
        // do something
    }
}

至于为什么使用 repeatOnLifecycle 而不是 launchWhenXXX,可以看看Jetpack MVVM七宗罪 之二:在 launchWhenX 中启动协程Jetpack MVVM 使用常见错误 这一篇文章。

参考