先来看看调度器Dispatchers,里面有4种CoroutineDispatcher。
// kotlinx.coroutines.Dispatchers
public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
@JvmStatic
public val IO: CoroutineDispatcher = DefaultIoScheduler
}
一、协程启动
1.GlobalScope
来看看使用 GlobalScope
的情况下,使用不同Dispatchers下,协程运行的情况。
// GlobalScope 使用
private const val TAG = "GlobalScope"
// 不传入 CoroutineDispatcher
fun globalScopeTest() = GlobalScope.launch {
//DefaultDispatcher-worker-1
Log.d(TAG, "inner globalScopeTest, 当前线程 = ${Thread.currentThread().name}")
}
// Dispatchers.Main
fun globalScopeTest1() = GlobalScope.launch(Dispatchers.Main) {
// main
Log.d(TAG, "inner globalScopeTest1, 当前线程 = ${Thread.currentThread().name}")
}
// Dispatchers.IO
fun globalScopeTest2() = GlobalScope.launch(Dispatchers.IO) {
// DefaultDispatcher-worker-1
Log.d(TAG, "inner globalScopeTest2, 当前线程 = ${Thread.currentThread().name}")
}
// Dispatchers.Default
fun globalScopeTest3() = GlobalScope.launch(Dispatchers.Default) {
// DefaultDispatcher-worker-2
Log.d(TAG, "inner globalScopeTest3, 当前线程 = ${Thread.currentThread().name}")
}
// Dispatchers.Unconfined
fun globalScopeTest4() = GlobalScope.launch(Dispatchers.Unconfined) {
delay(1000)
//如果不加delay, 当前线程 = main
//使用delay后, 当前线程 = kotlinx.coroutines.DefaultExecutor
Log.d(TAG, "inner globalScopeTest4, 当前线程 = ${Thread.currentThread().name}")
}
调用。
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
...
globalScopeTest()
globalScopeTest1()
globalScopeTest2()
globalScopeTest3()
globalScopeTest4()
}
}
结果如下:

简单总结:调用GlobalScope 启动协程运行的线程名有3种情况
DefaultDispatcher-worker-x (使用Dispatchers.Default和Dispatchers.IO,GlobalScope不传入 CoroutineDispatcher)
main(使用Dispatchers.Main,使用Dispatchers.Unconfined不调用delay(1000))
kotlinx.coroutines.DefaultExecutor(使用Dispatchers.Unconfined并调用delay(1000)后)
传入Dispatchers.Main则运行在main线程,传入Dispatchers.Default和Dispatchers.IO则运行在DefaultDispatcher-worker-x。传入Dispatchers.Unconfined则是调用者的线程,但是当在coroutine中第一个挂起之后,后面所在的线程将完全取决于调用挂起方法的线程。
要尽量避免使用GlobalScope,因为GlobalScope是生命周期是process级别的,即使Activity或Fragment已经被销毁,协程仍然在执行。
2.GlobalScope
// CoroutineScope 使用
private const val TAG = "CoroutineScope"
// Dispatchers.Main
fun coroutineScopeTest() = CoroutineScope(Dispatchers.Main).launch{
// main
Log.d(TAG, "inner coroutineScopeTest, 当前线程 = ${Thread.currentThread().name}")
}
// Dispatchers.IO
fun coroutineScopeTest1() = CoroutineScope(Dispatchers.IO).launch{
// DefaultDispatcher-worker-3
Log.d(TAG, "inner coroutineScopeTest1, 当前线程 = ${Thread.currentThread().name}")
}
// Dispatchers.Default
fun coroutineScopeTest2() = CoroutineScope(Dispatchers.Default).launch{
// DefaultDispatcher-worker-1
Log.d(TAG, "inner coroutineScopeTest2, 当前线程 = ${Thread.currentThread().name}")
}
// Dispatchers.Unconfined
fun coroutineScopeTest3() = CoroutineScope(Dispatchers.Unconfined).launch{
// main
Log.d(TAG, "inner coroutineScopeTest3, 当前线程 = ${Thread.currentThread().name}")
}
简单调用
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
...
coroutineScopeTest()
coroutineScopeTest1()
coroutineScopeTest2()
coroutineScopeTest3()
}
}
3.lifecycleScope
添加依赖
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
在MainActivity 中使用
private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity(){
fun lifecycleScopeTest() = lifecycleScope.launch {
// main
Log.d(TAG, "inner lifecycleScopeTest, 当前线程 = ${Thread.currentThread().name}")
}
fun lifecycleScopeTest1() = lifecycleScope.launch(Dispatchers.IO) {
// DefaultDispatcher-worker-2
Log.d(TAG, "inner lifecycleScopeTest1, 当前线程 = " + Thread.currentThread().name)
// 通过 withContext 来切换线程
withContext(Dispatchers.Main) {
// 当前线程 = main
Log.d(TAG, "inner withContext, 当前线程 = ${Thread.currentThread().name}")
}
}
// whenResumed和launchWhenResumed执行时机一样,在生命周期为 onResume 后调用
// 区别在于:whenResumed 可以有返回结果 , launchWhenResumed 返回的是Job对象
fun lifecycleScopeTest2() = lifecycleScope.launch {
whenResumed {
// main
Log.d(TAG, "inner lifecycleScopeTest2 whenResumed, 当前线程 = ${Thread.currentThread().name}")
}
}
fun lifecycleScopeTest3() = lifecycleScope.launchWhenResumed {
// main
Log.d(TAG, "inner lifecycleScopeTest3 launchWhenResumed, 当前线程 = ${Thread.currentThread().name}")
}
override fun onResume() {
super.onResume()
Log.d(TAG, "inner onResume")
}
}
4.viewModelScope
添加依赖
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
private const val TAG = "ViewModelScopeTest"
class ViewModelScopeTest: ViewModel(){
// 通过viewModelScope让框架管理生命周期,无需执行cancel
/**
* 没法在主线程完成的繁重操作
*/
fun launchDataLoad() {
// viewModelScope 默认使用 Dispatchers.Main
viewModelScope.launch {
// inner launch, 当前线程 = main
Log.d(TAG, "inner launch, 当前线程 = ${Thread.currentThread().name}")
sortList()
// 当前线程 = main 自动切换线程
Log.d(TAG, "inner launch after sortList, 当前线程 = ${Thread.currentThread().name}")
// 更新 UI
}
}
suspend fun sortList() = withContext(Dispatchers.Default) {
// 繁重任务
// DefaultDispatcher-worker-3
Log.d(TAG, "inner withContext, 当前线程 = ${Thread.currentThread().name}")
}
}
MainActivity中调用。
// MainActivity
private val viewModelScopeTest by lazy {
ViewModelProvider(this).get(ViewModelScopeTest::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModelScopeTest.launchDataLoad()
}
withContext 可以将当前线程从主线程切换到后台线程。然后执行完毕再切换回来到ui线程执行操作。
5.runBlocking
阻塞式不推荐使用,仅做了解。
另外除了launch,还有async可以启动协程,launch更常用。async可以返回闭包最后一行,并通过await()获取。
// runBlocking 阻塞式 不推荐
runBlocking {
// 当前线程 = main
Log.d(TAG, "inner runBlocking , 当前线程 = ${Thread.currentThread().name}")
launch(Dispatchers.IO) {
// 当前线程 = DefaultDispatcher-worker-1
Log.d(TAG, "inner runBlocking launch , 当前线程 = ${Thread.currentThread().name}")
}
/*
* async 和 launch 和差不多,就是比 launch多了一个返回值。
* 返回值 写在闭包 {}最后一行即可,然后通过 await()获取结果
* */
val job = async(Dispatchers.IO) {
// 当前线程 = DefaultDispatcher-worker-1
Log.d(TAG, "inner runBlocking async , 当前线程 = ${Thread.currentThread().name}")
23
}
val await = job.await()
// 当前线程 = main , await = 23
Log.d(TAG, "inner runBlocking , 当前线程 = ${Thread.currentThread().name} , await = $await")
}

最好是使用lifecycleScope和viewModelScope去启动协程。这2个扩展库可以更方便地处理生命周期相关的问题。
二、协程取消
在写线程的时候,其实是没有取消的,只有中断。协程的取消api为:cancel()。
协程中有一个Job,是对一个协程的句柄。创建的每个协程,不管是通过launch还是async来启动的,它都会返回一个Job实例,唯一标识该协程,并可以通过该Job管理其生命周期。
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
...
//Job和Dispatcher 组合成 CoroutineContext
val scope = CoroutineScope(Job() + Dispatchers.Default)
val job = scope.launch {
Log.d(TAG, "inner CoroutineScope before delay, 当前线程 = ${Thread.currentThread().name}")
//通过延时模拟耗时任务
delay(1000)
Log.d(TAG, "inner CoroutineScope after delay, 当前线程 = ${Thread.currentThread().name}")
}
job.cancel()
}
}
kotlinx.coroutines 中的所有挂起函数(例如 withContext 和 delay)都是可取消的。
由于调用job.cancel()取消,所以不执行after delay这条log。

三、异常处理
协程的异常处理,即使加了try-catch也会有问题,应用可能直接崩溃,而且完全无法捕获异常信息。
换句话说:如果把Java里的那一套异常处理机制,照搬到Kotlin协程里来,你一定会四处碰壁。因为在普通的程序当中,你使用try-catch就能解决大部分的异常处理问题,但是在协程当中,根据不同的协程特性,它的异常处理策略是随之变化的。
要注意以下几点:
- 1.协程的取消需要内部的配合。
- 2.不要轻易打破协程的父子结构!这一点,其实不仅仅只是针对协程的取消异常,而是要贯穿于整个协程的使用过程中。我们知道,协程的优势在于结构化并发,它的许多特性都是建立在这个特性之上的,如果我们无意中打破了它的父子结构,就会导致协程无法按照预期执行。
- 3.捕获了CancellationException以后,要考虑是否应该重新抛出来。在协程体内部,协程是依赖于CancellationException来实现结构化取消的,有的时候我们出于某些目的需要捕获CancellationException,但捕获完以后,我们还需要思考是否需要将其重新抛出来。
- 4.不要用try-catch直接包裹launch、async。这一点是很多初学者会犯的错误,考虑到协程代码的执行顺序与普通程序不一样,我们直接使用try-catch包裹launch、async,是不会有任何效果的。
- 5.灵活使用SupervisorJob,控制异常传播的范围。SupervisorJob是一种特殊的Job,它可以控制异常的传播范围。普通的Job,它会因为子协程当中的异常而取消自身,而SupervisorJob则不会受到子协程异常的影响。在很多业务场景下,我们都不希望子协程影响到父协程,所以SupervisorJob的应用范围也非常广。比如说Android当中的viewModelScope,它就使用了SupervisorJob,这样一来,我们的App就不会因为某个子协程的异常导致整个应用的功能出现紊乱。
- 6.使用CoroutineExceptionHandler处理复杂结构的协程异常,它仅在顶层协程中起作用。我们都知道,传统的try-catch在协程当中并不能解决所有问题,尤其是在协程嵌套层级较深的情况下。这时候,Kotlin官方为我们提供了CoroutineExceptionHandler作为补充。有了它,我们可以轻松捕获整个作用域内的所有异常。
image.png
更多具体部分,可参考《Kotlin编程第一课》
未处理协程中抛出的异常可能会导致应用崩溃。如果可能会发生异常,请在使用 viewModelScope 或 lifecycleScope 创建的任何协程主体中捕获相应异常。
https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=zh-cn
未处理协程中抛出的异常可能会导致应用崩溃。如果可能会发生异常,请在使用 viewModelScope 或 lifecycleScope 创建的任何协程主体中捕获相应异常。
注意:如需启用协程取消流程,请不要使用 CancellationException 类型的异常(不要捕获它们,或在被发现时总是重新抛出)。首选捕获特定类型的异常(如 IOException),而不是 Exception 或 Throwable 等一般类型。
class LoginViewModel(
private val loginRepository: LoginRepository
) : ViewModel() {
fun login(username: String, token: String) {
viewModelScope.launch {
try {
loginRepository.login(username, token)
// Notify view user logged in successfully
} catch (exception: IOException) {
// Notify view login attempt failed
}
}
}
}
总而言之,还是要具体情况具体分析。
另外,官方使用demo如下:
在 Android 应用中使用 Kotlin 协程
学习采用 Kotlin Flow 和 LiveData 的高级协程
参考链接:
【Android】在Activity中使用LifecycleScope替代GlobalScope
lifecycleScope 和viewModelScope
ViewModel中的简易协程:viewModelScope
破解 Kotlin 协程(2):协程启动篇
kotlin学习-Coroutines(协程)
Kotlin之协程coroutine使用(1)
Kotlin 协程一 —— 协程 Coroutine
协程的取消和异常Part1-核心概念
在 Android 中使用协程的最佳实践
《Kotlin编程第一课》
网友评论