今天在测试代码的时候,由于网络延迟高,手也快,没等数据加载出来,直接点了返回,发现了个问题,正好记录一下。
这段存在缺陷的代码是这样的:(代码已简化至最精简,所以当我贴出来报错信息的时候,就能看到具体是哪行了)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
GlobalScope.launch {
//这里执行了耗时操作,比如访问API,获取数据
viewModel.getDataFromApi()
withContext(Dispatchers.Main){
//这里执行刷新UI
displayData()
}
}
}
看起来很正常,但是在Api数据获取之前返回,会报错:CoroutineException
********* E/CoroutineException: Error in coroutine: Fragment CommonFragment{36b480f} (11ca1bed-50e9-4561-956d-e197092d5978)} not attached to a context.
看到这个错误,就知道使用协程报错了,所以:
第一步:找准报错代码行,给使用协程的地方加上Log,launch/withContext的地方都要加且要在他们的上一行,才能定位到哪行的协程报错(当我找到行数后,也发现写法没什么问题);
第二步:因为定位到了协程:GlobalScope ,这是 全局协程类:全局范围用于启动在整个应用程序生命周期内运行且不会过早取消的顶级协程。
第三步:因为它没有关联生命周期,所以这里肯定不行,即替换上: LifecycleCoroutineScope 即可。
完善后的代码如下:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 这里不在使用 GlobalScope
lifecycleScope.launch {
//这里执行了耗时操作,比如访问API,获取数据
viewModel.getDataFromApi()
withContext(Dispatchers.Main){
//这里执行刷新UI
displayData()
}
}
}
**************** 补充下:
因为看到报错的信息,就能发现是没有关联生命周期,导致当前页面关闭后,协程仍然执行,然后我也看下了源码,豁然开朗,其实很简单的,源码里都说明了这两个协程的使用区别:
#1. GlobalScope:
/**
* A global [CoroutineScope] not bound to any job.
*
* Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
* and are not cancelled prematurely.
* Another use of the global scope is operators running in [Dispatchers.Unconfined], which don't have any job associated with them.
*
* Application code usually should use an application-defined [CoroutineScope]. Using
* [async][CoroutineScope.async] or [launch][CoroutineScope.launch]
* on the instance of [GlobalScope] is highly discouraged.
*
* Usage of this interface may look like this:
*
* ```
* fun ReceiveChannel.sqrt(): ReceiveChannel = GlobalScope.produce(Dispatchers.Unconfined) {
* for (number in this) {
* send(Math.sqrt(number))
* }
* }
* ```
*/
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
注释的大概意思是:
* 全局CoroutineScope不受任何限制。
* 全局范围用于启动在整个应用程序生命周期内运行且不会过早取消的顶级协程。 全局范围的另一种用法是在Dispatchers.Unconfined中运行的运算符,它们没有任何关联的工作。
* 应用程序代码通常应使用应用程序定义的CoroutineScope。不建议在GlobalScope实例上使用异步或启动。
#1. LifecycleCoroutineScope:
/**
* [CoroutineScope] tied to a [Lifecycle] and
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
*
* This scope will be cancelled when the [Lifecycle] is destroyed.
*
* This scope provides specialised versions of `launch`: [launchWhenCreated], [launchWhenStarted],
* [launchWhenResumed]
*/
abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
internal abstract val lifecycle: Lifecycle
// 这里就不全部贴出来了
}
/**
* [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
*
* This scope will be cancelled when the [Lifecycle] is destroyed.
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
*/
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
注释的大概意思是:
* CoroutineScope与生命周期和Dispatchers.Main.immediate相关联,销毁生命周期时,将取消此作用域。
* 此范围提供了特殊的启动版本:launchWhenCreated,launchWhenStarted,launchWhenResumed
其实如果了解它们,在使用协程的时候就可以避免这样的报错。