一学就会的协程使用——基础篇(四)协程作用域

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()一般会被使用在对象生命周期结束的地方,比如ActivityonDestroy中,因为其方法调用后会取消其已启动的协程而且作用域对象再启动的协程将不会被执行,所以可用于防止内存泄漏;而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.png

7. 特别说明

不建议使用GlobalScope,更不建议每次为了启动线程而去新建作用域,在Android开发实际当中,开发场景下用lifeCycleScopeviewModelScope以及传递这两个引用可满足多数情况的需求。

一学就会的协程使用——基础篇

一学就会的协程使用——基础篇(一)协程启动

一学就会的协程使用——基础篇(二)线程切换

一学就会的协程使用——基础篇(三)初遇协程取消

一学就会的协程使用——基础篇(四)协程作用域(本文)

一学就会的协程使用——基础篇(五)再遇协程取消

一学就会的协程使用——基础篇(六)初识挂起

一学就会的协程使用——基础篇(七)初识结构化

一学就会的协程使用——基础篇(八)初识协程异常

一学就会的协程使用——基础篇(九)异常与supervisor

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容