Kotlin协程崩溃了,我也崩溃了-协程异常原理入门

Kotlin 协程异常处理机制

一直以为协程的异常机制很复杂,网上讲的术语一大堆,什么传播机制、结构化并发、协程作用域......看得我脑壳疼。但最近我研究了一下,也自己写代码测了一下,发现其实没有那么难。

简单入门,如有错误,欢迎指正

下面介绍几个核心的概念

1. 一个协程崩了,默认会把整个协程组都崩掉

为什么要强调这个呢,在Android里面线程不也是这样的吗?我们在开发Android的时候,一个线程崩了,整个应用就挂了。比如下面的代码:

kotlin 复制代码
// Android Studio中运行,应用启动就崩了
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        thread {
            throw new RuntimeException("子线程崩了 ❌");
        }
    }
}

一启动就崩了,所以我一直以为子线程崩溃了整个应用就会崩,直到我在IDEA中运行了同样的代码,如下:

kotlin 复制代码
fun main() {
    thread {
        Thread.sleep(100)
        throw RuntimeException("线程 A 崩了 ❌")
    }
    thread {
        Thread.sleep(1000)
        println("线程 B 还活着 ✅")
    }
    Thread.sleep(1000)
    println("主线程也活着 ✅")
}

会发现一个线程崩了之后,主线程和其他的子线程竟然都还活着,于是我查了一下相关的资料,终于搞明白了。

原来是因为:在Java中线程未捕获的异常会由UncaughtExceptionHandler处理,而Java默认是没有实现的,而Android实现了这个UncaughtExceptionHandler

看看Android是怎么实现的(在RuntimeInit中的KillApplicationHandler

java 复制代码
public void uncaughtException(Thread t, Throwable e) {
    try {
                        ...
    } catch (Throwable t2) {
        ...
    } finally {
        // Try everything to make sure this process goes away.
        Process.killProcess(Process.myPid());
        System.exit(10);
    }
}

Android在finally中执行了Process.killProcess(Process.myPid())System.exit(10),杀掉了线程,所以Android中线程异常应用是会崩溃的。

那么协程呢,运行下面这段代码:

kotlin 复制代码
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        coroutineScope {
            launch {
                delay(100)
                throw RuntimeException("协程 A 崩了 ❌")
            }
            launch {
                delay(1000)
                println("协程 B 应该活着 ❌,但没机会执行")
            }
        }
    }
}

运行之后发现,一个协程崩了,其他的协程也没有继续执行了。

原来在Kotlin中 由此我们可以总结下:

  • 在线程中一个线程崩,不会影响其他线程。
  • 在Android中由于系统的处理,无论哪个线程崩,整个应用都会退出。(除非自己调用了setDefaultUncaughtExceptionHandler)
  • 而协程和线程是不一样的。如果在同一个 coroutineScope 里跑多个协程,只要一个出错,其他协程也会被一起取消

2. 如果不想都崩掉,就用 supervisorScope

Kotlin 提供了一个 supervisorScope(或者 SupervisorJob()),它的意思是:

"我只管我自己,我崩了不管别人,别人崩了也别来拉我下水。"

kotlin 复制代码
fun main() {
    runBlocking {
        supervisorScope {
            launch {
                delay(100)
                throw RuntimeException("协程 A 崩了 ❌")
            }
            launch {
                delay(1000)
                println("协程 B 仍然正常 ✅")
            }
        }
    }
}

或者用SupervisorJob

kotlin 复制代码
fun main() {
    runBlocking {
        val supervisor = SupervisorJob()
        val scope = CoroutineScope(Dispatchers.Default + supervisor)

        scope.launch {
            delay(100)
            throw RuntimeException("协程 A 崩了 ❌")
        }

        scope.launch {
            delay(1000)
            println("协程 B 仍然正常 ✅")
        }

        delay(2000) // 等待所有子协程完成
    }
}

这段代码就能明显看出来:一个崩了,另一个还能活得好好的


3. 最后的兜底:CoroutineExceptionHandler

如果忘了 try-catch,协程崩了可以用这个兜底。它会在 launch 抛异常时触发,但对 async 是无效的。

kotlin 复制代码
val handler = CoroutineExceptionHandler { _, exception ->
    println("捕获到异常:${exception.message} ✅")
}

fun main() = runBlocking {
    val job = launch(handler) {
        throw RuntimeException("我崩了 ❌")
    }
    job.join()
}

但下面这种 async 是不会被 handler 处理的:

kotlin 复制代码
val handler = CoroutineExceptionHandler { _, exception ->
    println("Handler 触发了:${exception.message}")
}

fun main() {
    runBlocking {
        val scope = CoroutineScope(handler)
        scope.launch {
            throw RuntimeException("协程 A 崩了 ❌")
        }
        val deferred = scope.async {
            throw RuntimeException("async 崩了 ❌")
        }
        scope.launch {
            delay(1000)
            println("协程 B 应该活着 ❌,但没机会执行")
        }
        deferred.await()
        // 必须用 try-catch,否则照样崩
//    try {
//        deferred.await()
//    } catch (e: Exception) {
//        println("自己捕获 async 异常 ✅")
//    }
        Unit
    }
}

5. viewModelScopelifecycleScope

  • viewModelScope 内部使用了 SupervisorJob,所以其中的子协程是互不影响的,崩一个不会带崩其他。
kotlin 复制代码
return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
  • lifecycleScope也是 supervisor
kotlin 复制代码
val newScope = LifecycleCoroutineScopeImpl(
    this,
    SupervisorJob() + Dispatchers.Main.immediate
)

总结一下

  • 一个线程崩了,默认不会影响其他线程,但是Android做了特殊处理,会将应用杀掉
  • 一个协程崩了,默认会 cancel同scope里的全部协程(不像线程那么"独立");
  • supervisorScope(或 SupervisorJob)可以避免"崩一个全挂";
  • CoroutineExceptionHandler 可以兜住 launch 异常,但兜不了 async,async 要自己 try-catch。
  • Android 中 viewModelScopelifecycleScope 都用了 SupervisorJob

相关推荐
踢球的打工仔16 小时前
PHP面向对象(7)
android·开发语言·php
安卓理事人16 小时前
安卓socket
android
安卓理事人1 天前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学1 天前
Android M3U8视频播放器
android·音视频
q***57741 天前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober1 天前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿1 天前
关于ObjectAnimator
android
zhangphil1 天前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我1 天前
从头写一个自己的app
android·前端·flutter
lichong9511 天前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端