1. 引言
其实,在每次启动协程都需要一个协程作用域对象,在此处之前的实践代码,用的都是GlobalScope
这个单例启动的协程,为的是不要过早地接触协程作用域,以至于产生对协程作用域的使用误解!
承接前文,对于每次启动的协程Job对象都要收集保存后才能取消,事实上,在协程的管理维度上,才是协程作用域大展拳脚的地方!
2. 实践代码说明
首先,在Activity中声明并初始化一个协程作用域对象
/**
* 其实这种写法本质等同于调用 MainScope(),这里的写法参考[MainScope]函数注释中的例子建议
*/
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
至于这里为什么用SupervisorJob()
,则又是另一个话题了,后续的第九篇才会再次讨论这方面的内容。
这里只需要知道,在Activity中有且仅有一个协程作用域对象!并且这个协程作用域对象在onDestroy
中调用了其取消方法
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
接下来,为了方便对比验证和代码重用,封装下面的方法:
private fun scopeLaunch(scope: CoroutineScope, calledMsg: String) {
"clicked $calledMsg, scopeRef:${scope.objIdentityStr}".let {
myLog(it)
updateMsgShow(buildUIMsg(it), viewBinding.msgShowRv)
}
scope.launch(Dispatchers.IO) {
"Coroutine IO runs (from ${calledMsg})".let {
myLog(it)
}
var curMillis = System.currentTimeMillis()
val targetMilli = curMillis + FIVE_SECONDS
while (curMillis < targetMilli) {
ensureActive()
val msg = "looping (from ${calledMsg})"
myLog(msg)
curMillis = System.currentTimeMillis()
}
"loop finished (from ${calledMsg})".let {
myLog(it)
stringBuilder.append(buildUIMsg(it))
}
}
}
总体逻辑与前一章节的执行逻辑一致,就是启动一个协程,5秒内不断循环输出log,循环结束后再输出log,循环中间带有取消协作函数。
但是启动协程时不再使用GlobalScope
,而是用了函数参数中传递的协程作用域对象去启动!
再强调一遍,GlobalScope是不被推荐使用的!
注意上述封装还有一个重点内容,那就将不再获取launch的返回的Job对象,也不再有收集容器,而协程的取消部分逻辑,将交于页面内唯一的协程作用域对象scope!
同时,本文协程作用域有两种取消协程的方式,分别是:
scope.cancel() /* 取消仍未结束的协程,触发后作用域对象再启动协程的执行代码不再执行 */
scope.coroutineContext.cancelChildren() /* 取消仍未结束的协程,触发后作用域对象再启动协程的执行代码仍会执行 */
这两种取消方式,本文也将有所讨论和验证。
3. 实践过程说明
viewBinding.launchByScope -> {
scopeLaunch(scope, "launchByScope")
}
通过点击按钮,传入页面属性scope对象,进行协程启动。
然后可以通过点击不同的按钮,可以分别触发两种取消方式:
viewBinding.cancelByScope -> {
"Clicked cancelByScope".let {
myLog(it)
}
scope.cancel()
}
viewBinding.cancelChildren -> {
"Clicked cancelChildren".let {
myLog(it)
}
scope.coroutineContext.cancelChildren()
}
点了启动按钮以后,5秒以内无论点击哪一种取消按钮,都会发现页面启动的协程都会在取消按钮点击以后不再有log输出。同时,无论在点击取消按钮前点击了多少次启动按钮,启动的所有协程均在取消按钮被点击以后不再有log输出!
是不是直觉上比较奇怪?明明没有收集处理每次启动的协程,为什么点击取消按钮后,协程取消生效了呢?
其实不奇怪,这便是协程作用域设计的主要作用的体现:
协程作用域是结构化并发的规约(本句话出自于协程作用域源码注释的直译)
结构化并发又是什么?结构化并发也是协程中一个非常丰富且强大的内容,当前只要理解,协程的取消也是结构化并发的其中一个部分。
4. 协程作用域的取消方式对比
如果取消协程时点击的是cancelByScope
按钮,那么后续再点击启动按钮,将再也不会有协程启动,自然也不会有log输出。
如果取消协程时点击的是cancelChildren
按钮,那么后续再点击启动按钮,将继续有协程启动和log输出(前提是cancelByScope从来没有被点击过)。
这时候,根据结果,不妨再回头看看协程作用域两种取消协程的方式对比?
事实上,一般而言,scope.cancel()
一般会被使用在对象生命周期结束的地方,比如Activity
的onDestroy
中,因为其方法调用后会取消其已启动的协程而且作用域对象再启动的协程将不会被执行,所以可用于防止内存泄漏;而scope.coroutineContext.cancelChildren()
主要用于仅仅取消当前作用域对象已经启动的协程的情景,不可用于防止内存泄漏。
如果目标是取消已启动协程,那么scope.coroutineContext.cancelChildren()
才是合理的;相对地,scope.cancel()
要避免在开发逻辑中主动调用,而是在对象生命周期管理方法中被动调用!
5. 协程作用域的作用范围
不要以为协程作用域对于协程的取消是万能的,事实上,一个协程作用域对象,仅能取消由此对象启动的协程,而对其他协程作用域对象启动的协程无能为力!
为此,有以下实践代码:
viewBinding.launchByGlobalScope -> {
scopeLaunch(GlobalScope, "launchByGlobalScope")
}
viewBinding.launchByNewScope -> {
scopeLaunch(CoroutineScope(Dispatchers.Main), "launchByNewScope")
}
使得,这两个启动协程的按钮,分别向scopeLaunch
函数传入了GlobalScope
对象和新构建的协程作用域对象,点击这两者产生的协程后,再点击页面的取消按钮,会发生启动的协程根本不会受到取消的影响。
其实这很好理解,协程作用域对象都是独立的个体,所以只能管好自己启动的协程。
6. 样例工程代码
代码样例Demo,见Github:https://github.com/TeaCChen/CoroutineStudy
本文示例代码,如觉奇怪或啰嗦,其实为CoroutineScopeActivity.kt
中的代码摘取主要部分说明,在demo代码当中,为提升细节内容,有更加多的封装和输出内容。
本文的页面截图示例如下:
image-4-1.png7. 特别说明
不建议使用GlobalScope
,更不建议每次为了启动线程而去新建作用域,在Android开发实际当中,开发场景下用lifeCycleScope
和viewModelScope
以及传递这两个引用可满足多数情况的需求。
一学就会的协程使用——基础篇(四)协程作用域(本文)
网友评论