kotlin协程在Android中的基础应用
通过前面的三个章节,现在我们已经了解了kotlin
协程的基本使用和相关基础知识点。如:
- 协程的基础使用方式和基本原理。
CoroutineContext
:协程上下文中包含的Element
以及下上文的作用,传递。CoroutineDispatcher
:协程调度器的使用CoroutineStart
:协程启动模式在不同模式下的区别CoroutineScope
:协程作用域的分类,以及不同作用域下的异常处理。- 挂起函数以及
suspend
关键字的作用,以及Continuation
的挂起恢复流程。 CoroutineExceptionHandler
:协程异常处理,结合supervisorScope
和SupervisorJob
的使用。
这一章节中,我们将主要讲解kotlin协程在Android中的基础使用。我们先引入相关扩展库组件库:
implementation "androidx.activity:activity-ktx:1.2.2"
implementation "androidx.fragment:fragment-ktx:1.3.3"
Android使用kotlin协程
我们在之前的章节中使用协程的方式都是通过runBlocking
或者使用GlobalScope
的launch
、async
方式启动,当然也可以通过创建一个新的CoroutineScope
,然后通过launch
或者async
方式启动一个新的协程。我们在讲解协程异常处理
的篇章中就提到,通过SupervisorJob
和CoroutineExceptionHandler
实现了一个和supervisorScope
相同的作用域。
private fun testException(){
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName].toString()} 处理异常 :$throwable")
}
val supervisorScope = CoroutineScope(SupervisorJob() + exceptionHandler)
with(supervisorScope) {
launch{
}
//省略...
}
}
在第一节中我们提到runBlocking
它会将常规的阻塞代码连接到一起,主要用于main函数和测试中。而GlobalScope
又是一个全局顶级协程,我们在之前的案例中基本都使用这种方式。但是这个协程是在整个应用程序生命周期内运行的,如果我们用GlobalScope
启动协程,我们启动一个将会变得极其繁琐,而且需要对于各种引用的处理以及管控异常取消操作。
我们可以先忽略CoroutineExceptionHandler
协程异常处理。因为不管是任何方式启动协程,如果不在程上下文中添加CoroutineExceptionHandler
,当产生未捕获的异常时都会导致应用崩溃。
那么下面代码会出现什么问题?
private fun start() {
GlobalScope.launch{
launch {
//网络请求1...
throw NullPointerException("空指针")
}
val result = withContext(Dispatchers.IO) {
//网络请求2...
requestData()
"请求结果"
}
btn.text = result
launch {
//网络请求3...
}
}
}
-
因为我们的
GlobalScope
默认使用的是Dispatchers.Default
,这会导致我们在非主线程上刷新UI。 -
子协程产生异常会产生相互干扰。子协程异常取消会导致父协程取消,同时其他子协程也将会被取消。
-
如果我们这个时候
activity
或者framgent
退出,因为协程是在GlobalScope
中运行,所以即使activity
或者framgent
退出,这个协程还是在运行,这个时候会产生各种泄露问题。同时此协程当执行到刷新操作时,因为我们的界面已经销毁,这个时候执行UI刷新将会产生崩溃。
如果我们要解决上面的问题。我们得这么做:
var job:Job? = null
private fun start() {
job = GlobalScope.launch(Dispatchers.Main + SupervisorJob()) {
launch {
throw NullPointerException("空指针")
}
val result = withContext(Dispatchers.IO) {
//网络请求...
"请求结果"
}
launch {
//网络请求3...
}
btn.text = result
}
}
override fun onDestroy() {
super.onDestroy()
job?.cancel()
}
我们先需要通过launch
启动时加入Dispatchers.Main
来保证我们是在主线程刷新UI,同时还需要再GlobalScope.launch
的协程上下文中加入SupervisorJob
来避免子协程的异常取消会导致整个协程树被终结。 最后我们还得把每次
通过GlobalScope
启动的Job
保存下来,在activity
或者framgent
退出时调用job.cancel
取消整个协程树。这么来一遍感觉还行,但是我们不是写一次啊,每次写的时候会不会感觉超麻烦,甚至怀疑人生。
所以官方在kotlin协程中提供了一个默认在主线程运行的协程:MainScope
,我们可以通过它来启动协。
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
我们可以看到MainScope
的创建默认就使用了SupervisorJob
和 Dispatchers.Main
。说明我们可以通过MainScope
来处理UI组件刷新。同时由于MainScope
采用的是SupervisorJob
,所以我们各个子协程中的异常导致的取消操作并不会导致MainScope
的取消。这就很好的简化了我们通过GlobalScope
去启动一个协程的过程。
private val mainScope = MainScope()
private fun start() {
mainScope.launch {
launch {
throw NullPointerException("空指针")
}
val result = withContext(Dispatchers.IO) {
//网络请求...
"请求结果"
}
launch {
//网络请求3...
}
btn.text = result
}
}
override fun onDestroy() {
super.onDestroy()
mainScope.cancel()
}
通过使用MainScope
我们是不是省略了很多操作。同时我们也不需要保存每一个通过MainScope
启动的Job
了,直接在最后销毁的时候调用mainScope.cancel()
就能取消所有通过mainScope
启动的协程。
这里多提一点:可能这里有的人会想,我使用GlobalScope
也不保存启动的Job
,直接GlobalScope.cancel
不行吗?如果是这样的话,那么恭喜你喜提超级崩溃BUG一个。这里就不扩展了。可以自己动手去试试,毕竟实践出真理。
那可能还有人想,我连创建MainScope
都懒得写,而且脑子经常不好使,容易忘记调用mainScope
进行cancel
操作怎么办。
官方早就为我们这些懒人想好了解决方案,这个时候我们只需要再集成一个ktx运行库就可以了。
在Activity与Framgent中使用协程
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
这个时候我们就可以在activity
或者framgent
直接使用lifecycleScope
进行启动