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

相关推荐
Lei活在当下4 小时前
【业务场景架构实战】7. 多代智能手表适配:Android APP 表盘编辑页的功能驱动设计
android·设计模式·架构
手机不死我是天子7 小时前
《Android 核心组件深度系列 · 第 2 篇 Service》
android
前行的小黑炭7 小时前
Compose页面切换的几种方式:Navigation、NavigationBar+HorizontalPager,会导致LaunchedEffect执行?
android·kotlin·app
前行的小黑炭8 小时前
Android :Comnpose各种副作用的使用
android·kotlin·app
BD_Marathon1 天前
【MySQL】函数
android·数据库·mysql
西西学代码1 天前
安卓开发---耳机的按键设置的UI实例
android·ui
maki0771 天前
虚幻版Pico大空间VR入门教程 05 —— 原点坐标和项目优化技巧整理
android·游戏引擎·vr·虚幻·pico·htc vive·大空间
千里马学框架1 天前
音频焦点学习之AudioFocusRequest.Builder类剖析
android·面试·智能手机·车载系统·音视频·安卓framework开发·audio
fundroid1 天前
掌握 Compose 性能优化三步法
android·android jetpack
TeleostNaCl1 天前
如何在 IDEA 中使用 Proguard 自动混淆 Gradle 编译的Java 项目
android·java·经验分享·kotlin·gradle·intellij-idea