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

相关推荐
DogDaoDao8 小时前
Android 硬件编码器参数完全指南:MediaCodec 深度解析
android·音视频·视频编解码·h264·硬编码·视频直播·mediacodec
JohnnyDeng948 小时前
Android 自定义 View:Canvas 绘图与事件分发深度解析
android
Android小码家12 小时前
Framework之Launcher小窗开发
android·framework·虚拟屏·小窗
赏金术士12 小时前
第七章:状态管理实战与架构总结
android·ui·kotlin·compose
颂love13 小时前
MySQL的执行流程
android·数据库·mysql
云起SAAS17 小时前
抖音小游戏源码 - 消消乐 | 含激励广告+成就系统 | 开箱即用商业级消除游戏模板
android·游戏·广告联盟·看激励广告联盟流量主·抖音小游戏源码 - 消消乐
大貔貅喝啤酒19 小时前
基于Windows下载安装Android Studio 3.3.2版本教程(2026详细图文版)
android·java·windows·android studio
程序员码歌19 小时前
OpenSpec 到 Superpowers:AI 编码从说清到做对
android·前端·人工智能
2501_9151063219 小时前
深入解析无源码iOS加固原理与方案,保护应用安全
android·安全·ios·小程序·uni-app·cocoa·iphone
黄林晴1 天前
重磅官宣:Android UI 开发正式进入 Compose-first 时代
android·google io