Kotlin协程-CoroutineScope协程作用域
Kotlin协程系列:
前文中我们演示了协程的基本使用,和协程的上下文环境,而之前的示例我们多半都是使用的是 GlobalScope 来启动的协程,难道没有别的方法启动协程了吗?当然有的,这一期就主要介绍一下协程作用域的概率与常见的一些协程作用域。
协程的作用域 Scope 涉及到 代码运行范围,协程的生命周期,自动管理协程的生命周期等。下面我们先看看协程的生命周期与作用范围
一、协程的生命周期
当我们创建一个协程的时候,会返回一个Job对象,不管是通过返回值管理,还是通过 launch 的构造方法的形式管理,其实是一样的。
我们通过Job就可以获取当前协程的运行状态,还可以随时取消协程。
协程的状态查询
- isActive
- isCompleted
- isCancelled
常用的协程操作:
- cancel 用于Job的取消,取消协程
- start 用于启动一个协程,让其到达Active状态
- invokeOnCompletion 添加一个监听,当工作完成或者异常时会调用
- join 阻塞并等候当前协程完成
协程不是默认创建就启动了吗? 怎么还有一个 start 方法 。
其实协程默认是启动的,但是我们可以创建一个懒加载的协程,手动start才开启协程。
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
YYLogUtils.w("执行在协程中...")
delay(1000L)
YYLogUtils.w("执行完毕...")
}
job.start()
协程的取消,我们之前也讲到过,一般我们可以手动的调用 calcel 或者在onDestory的时候调用 calcel:
var job: Job = Job()
...
GlobalScope.launch(job) {
YYLogUtils.w("执行在协程中...")
delay(1000L)
YYLogUtils.w("执行完毕...")
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
协程执行完的回调 invokeOnCompletion 也是我们常用的监听,在正常执行完毕,或者异常执行完毕都会回调这个方法。
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
YYLogUtils.e(throwable.message ?: "Unkown Error")
}
val job = GlobalScope.launch(Dispatchers.Main + exceptionHandler) {
YYLogUtils.w("执行在另一个协程中...")
delay(1000L)
val num = 9/0
YYLogUtils.w("另一个协程执行完毕...")
}
job.invokeOnCompletion {
YYLogUtils.w("完成或异常的回调")
}
没有异常的回调:
加入9/0的异常代码之后的回调:
二、协程的作用域
当我们创建一个协程的时候,都会需要一个CoroutineScope,它是协程的作用域,我们一般使用它的launch函数以及async函数去进行协程的创建。
2.1 常用的协程作用域
之前我们都是通过 GlobalScope 来启动一个协程的,其实这样使用在 Android 开发中并不好。因为是全局的作用域。
在 Android 开发过程中,我们需要理解一些协程代码运行的范围。而所有的Scope 如GlobalScope 都是 CoroutineScope 的子类,我们的协程创建都需要这样一个 CoroutineScope 来启动。
同时我们还有其他的一些作用范围的 CoroutineScope 对象。
- GlobeScope:全局范围,不会自动结束执行。
- MainScope:主线程的作用域,全局范围
- lifecycleScope:生命周期范围,用于activity等有生命周期的组件,在DESTROYED的时候会自动结束。
- viewModelScope:viewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束
不同的Scope有不同的使用场景,下面我会仔细讲解。
2.2 coroutineScope vs runBlocking
使用 coroutineScope 构建器声明自己的作用域。它会创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束。runBlocking 与 coroutineScope 的主要区别在于后者在等待所有子协程执行完毕时不会阻塞当前线程。
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R =
suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
该函数被 suspend 修饰,是一个挂起函数,前面我们说了挂起函数是不会阻塞线程的,它只会挂起协程,而不阻塞线程。
前面我们说了 runBlocking 是桥接阻塞代码与挂起代码之前的桥梁,其函数本身是阻塞的,但是可以在其内部运行 suspend 修饰的挂起函数。在内部所有子协程运行完毕之前,他是阻塞线程的。
2.3 怎么使用 coroutineScope
比如我们定义一个 suspend 标记的方法,内部执行一个协程,我们看看 coroutineScope 使用示例:
我们定义一个 suspend 方法,内部返回 coroutineScope 作用域对象,内部执行的是协程。
private suspend fun saveSth2Local(coroutineBlock: (suspend CoroutineScope.() -> String)? = null): String? {
return coroutineScope {
// coroutineBlock!!.invoke(this)
// coroutineBlock?.invoke(this)
// if (coroutineBlock != null) {
// coroutineBlock.invoke(this)
// }
coroutineBlock?.let { block ->
block()
}
}
}
注释的几种代码都是同样的效果,这么写为了更方便大家理解。传入的 coroutineBlock 是一个高阶扩展函数,如果对这种写法比较陌生可以看看我的这一篇文章。
那么在使用我们这一个函数的时候就可以这么使用:
MainScope().launch {
YYLogUtils.w("执行在一个协程中...")
val result = saveSth2Local {
async(Dispatchers.IO) {
"123456"
}.await()
}
YYLogUtils.w("一个协程执行完毕... result:$result")
}
打印结果:
2.4 为什么要用 coroutineScope
有人会继续发问,我们为什么需要使用 coroutineScope 来实现呢?直接在一个协程中写完不香吗?
其实这么写是为了把一些比较耗时的多个任务拆分为不同的小任务,指定了一个作用域,在此作用域下面如果取消,就整个作用域都取消,如果异常则整个作用域内的协程都取消。简单的说就是更加的灵活。
我们举例说明一下,比如一个 coroutineScope 下面有多个任务
suspend fun showSomeData() = coroutineScope {
val data1 = async {
delay(2000)
100
}
val data2 = async {
delay(3000)
20
}
val num = withContext(Dispatchers.IO) {
delay(3000)
val random = Random(10)
data1.await() + data2.await() + random.nextInt(100)
}
YYLogUtils.w("num:"+num)
}
上面的代码意思就是data1 和 data2 并发,等它们完成之后获取到 num 打印出来。
为什么这么写,就是很明显的用到作用域的一个概念,如果data1 data2 失败,那么 withContext 就不会执行了,如果 random 异常,那么整个协程作用域内的任务都会取消。
这就是作用域的作用!
2.5 父子协程作用域
当一个协程在其它协程在中启动的时候,那么我们可以理解它是子协程吗,包裹它的就是它的父协程, 它将通过 CoroutineScope.coroutineContext 来继承了父协程的上下文,并且这个新协程的 Job 将会成为父协程作业的子作业。当一个父协程被取消的时候,所有它的子协程也会被递归的取消。
多的不说,代码运行演示一番:
val job = CoroutineScope(Dispatchers.Main).launch {
async(Dispatchers.IO) {
YYLogUtils.w("切换到一个协程1")
delay(5000)
YYLogUtils.w("协程1执行完毕")
}
launch {
YYLogUtils.w("切换到一个协程2")
delay(2000)
YYLogUtils.w("协程2执行完毕")
}
GlobalScope.launch {
YYLogUtils.w("切换到一个协程3")
delay(3000)
YYLogUtils.w("协程3执行完毕")
}
MainScope().launch {
YYLogUtils.w("切换到一个协程4")
delay(4000)
YYLogUtils.w("协程4执行完毕")
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
这么写大家应该都能看懂了,在一个父协程中并发启动4个子协程,毫无疑问,他们的执行顺序为:
没毛病,但是我们启动完四个子协程之后,我们把父协程cancel掉,我们看能正常的关闭子协程吗?
此时我们关闭父协程,看看是否能成功的关闭子协程
结果就是,确实父协程关闭能关闭子协程,但是又不能完全关闭。
因为使用 GlobalScope MainScope 来启动一个协程时,则新协程的作业没有父作业。 因此它与这个启动的作用域无关且独立运作,与它的父协程没有关联上。
那我们能不能让不能取消的子协程强行跟父协程关联上?还有这操作?看我操作:
val job = CoroutineScope(Dispatchers.Main).launch {
async(Dispatchers.IO) {
YYLogUtils.w("切换到一个协程1")
delay(5000)
YYLogUtils.w("协程1执行完毕")
}
launch {
YYLogUtils.w("切换到一个协程2")
delay(2000)
YYLogUtils.w("协程2执行完毕")
}
GlobalScope.launch {
YYLogUtils.w("切换到一个协程3")
delay(3000)
YYLogUtils.w("协程3执行完毕")
}
MainScope().launch(job!!) {
YYLogUtils.w("切换到一个协程4")
delay(4000)
YYLogUtils.w("协程4执行完毕")
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
例如我们把 MainScope 启动的时候传入上下文环境,既然它不能继承父协程的上下文,我们手动的设置给它,行不行?
看下Log:
此时我们关闭父协程,看看是否能成功的关闭子协程
可以看到 MainScope 所在的协程就可以跟随父协程一起取消了。
这么说,大家应该能理解透彻了吧。下面我们就一起看看 MainScope 和 GlobalScope 是什么鬼,怎么就不能继续父协程的上下文了呢?,它们之间又有什么区别?
三、MainScope vs GlobalScope
都是全局的作用域,但是他们有区别。如果不做处理他们都是运行在全局无法取消的,但是GlobalScope是无法取消的,MainScope是可以取消的。
GlobalScope 的源码如下:
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
可以看到它的上下文对象是 EmptyCoroutineContext 对象,并没有Job对象,所以我们无法通过 Job 对象去cancel 此协程。所以他是无法取消的进程级别的协程。除非有特殊的需求,我们都不使用此协程。
MianScope 的源码如下:
public fun MainScope(): CoroutineScope =
ContextScope(SupervisorJob() + Dispatchers.Main)
可以看到它的上下文对象是 SupervisorJob + 主线程构成的。如果对 + 号不了解,可以看本系列的第二篇。所以我们说它是一个可以取消的全局主线程协程。
按照上面的代码,我们就能这么举例说明:
var mainScope= MainScope()
mainScope.launch {
YYLogUtils.w("执行在一个协程中...")
val result = saveSth2Local {
async(Dispatchers.IO) {
"123456"
}.await()
}
YYLogUtils.w("一个协程执行完毕... result:$result")
}
override fun onDestroy() {
super.onDestroy()
mainScope.cancel()
}
四、viewModelScope
viewModelScope 只能在ViewModel中使用,绑定ViewModel的生命周期。使用的时候需要导入依赖:
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
源码如下:
private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
可以看到viewModelScope使用 SupervisorJob 而不是用 Job。 为了 ViewModel 能够取消协程,需要实现 Closeable 接口 viewModelScope 默认使用 Dispatchers.Main, 方便 Activity 和 Fragment 更新 UI
private final Map<String, Object> mBagOfTags = new HashMap<>();
<T> T getTag(String key) {
synchronized (mBagOfTags) {
return (T) mBagOfTags.get(key);
}
}
<T> T setTagIfAbsent(String key, T newValue) {
T previous;
synchronized (mBagOfTags) {
previous = (T) mBagOfTags.get(key);
if (previous == null) {
mBagOfTags.put(key, newValue);
}
}
T result = previous == null ? newValue : previous;
if (mCleared) {
closeWithRuntimeException(result);
}
return result;
}
@MainThread
final void clear() {
mCleared = true;
if (mBagOfTags != null) {
for (Object value : mBagOfTags.values()) {
closeWithRuntimeException(value);
}
}
onCleared();
}
private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
类通过 HashMap 存储 CoroutineScope 对象,取消的时候, 在 clear() 方法中遍历调用 closeWithRuntimeException 取消了viewModelScope 的协程。
代码可以说是简单又清晰
使用的时候和 GlobalScope 的使用一样的,需要注意的是无需我们手动的cancel了。
使用的时候平替 GlobalScope 即可完成:
viewModelScope.launch{
YYLogUtils.w("执行在另一个协程中...")
delay(1000L)
YYLogUtils.w("另一个协程执行完毕...")
}
五、lifecycleScope
lifecycleScope只能在Activity、Fragment中使用,会绑定Activity和Fragment的生命周期。使用的时候需要导入依赖:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
它的基本使用和 viewModelScope 是一样的。但是它多了生命周期的的一些感知。
例如在Resume的时候启动协程:
fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenResumed(block)
}
如何实现的,大家可以看看 LifecycleController 的源码:
@MainThread
internal class LifecycleController(
private val lifecycle: Lifecycle,
private val minState: Lifecycle.State,
private val dispatchQueue: DispatchQueue,
parentJob: Job
) {
private val observer = LifecycleEventObserver { source, _ ->
if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) {
// cancel job before resuming remaining coroutines so that they run in cancelled
// state
handleDestroy(parentJob)
} else if (source.lifecycle.currentState < minState) {
dispatchQueue.pause()
} else {
dispatchQueue.resume()
}
}
init {
// If Lifecycle is already destroyed (e.g. developer leaked the lifecycle), we won't get
// an event callback so we need to check for it before registering
// see: b/128749497 for details.
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
handleDestroy(parentJob)
} else {
lifecycle.addObserver(observer)
}
}
//...
}
在init初始化的时候,添加LifecycleEventObserver监听,对生命周期进行了判断,当大于当前状态的时候,也就是生命周期执行到当前状态的时候,会调用dispatchQueue.resume()执行队列,也就是协程开始执行。
相比 viewModelScope 同样的无需我们手动的取消,但是它又多了一些Activity,Fragment的生命周期感知。
使用的时候应该也是很好理解的:
lifecycleScope.launch{
YYLogUtils.w("执行在另一个协程中...")
delay(1000L)
YYLogUtils.w("另一个协程执行完毕...")
}
lifecycleScope.launchWhenResumed {
YYLogUtils.w("执行在另一个协程中...")
delay(3000L)
YYLogUtils.w("另一个协程执行完毕...")
}
六、自定义协程作用域
我们有了 lifecycleScope 和 viewModelScope 真的是太方便了,不管是在UI页面中,还是在ViewModel中处理异步逻辑,都是非常的方便。
但是项目中,除了ViewModel 和 Activity / Fragment 之外,还有其他的UI布局,比如PopupWindow Dialog。那怎么办?
有人说把Activity对象当参数传递进去,然后就能使用 lifecycleScope 啦,这...没毛病,但是其实我们有更好的做法。就是上面我们讲到的 MainScope() 。
它默认的上下文是我们的主线程加上一个 SupervisorJob 管理的,比如我们在Dialog 中就可以通过这样来管理协程作用域啦。
class CancelJobDialog() : DialogFragment(), CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
@SuppressLint("InflateParams")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
dialog?.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
return inflater.inflate(R.layout.dialog_cancel_job, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val mNoTv = view.findViewById<TextView>(R.id.btn_n)
val mYesTv = view.findViewById<TextView>(R.id.btn_p)
mNoTv.click {
dismiss()
}
mYesTv.click {
doSth()
}
}
private fun doSth() {
launch{
YYLogUtils.w("执行在另一个协程中...")
delay(1000L)
YYLogUtils.w("另一个协程执行完毕...")
}
dismiss()
}
override fun onDismiss(dialog: DialogInterface) {
cancel()
super.onDismiss(dialog)
}
我们实现一个 CoroutineScope 作用域接口,然后使用委托的属性把 MainScope 的实现给它。这样这个 Dialog 就是一个协程的作用域了。
在内部就能 launch N多个子协程了,注意我们在 onDismiss 的时候把主协程都取消掉了,按照我们前面讲到的父子协程的作用域。那么它内部 launch 的多个子协程就能一起取消了。
这样就能简单的实现一个自定义的协程作用域了,当然实现自定义协程作用域的方法有多种,这里只是介绍最简单的一种,由于时间与篇幅的原因,后面会再次提到。
总结
上一篇我们讲了协程的基本使用,掌握的就是协程启动的几种方式,切换线程的几种方式,异步与同步的执行,和挂起函数,阻塞与非阻塞的概念。还讲到了协程的上下文,原来调度线程,管理协程的Job等都是上下文的实现。
这一篇我们进一步理解协程的作用域,原来作用域分这么多类型,理解了父子协程是怎么处理取消逻辑的,coroutineScope的作用域范围的使用等等。
其实到这里协程的讲解就差不多了,下一篇我们会讲一下协程的场景使用,网络请求的使用,自定义协程,与协程的并发与锁等相关用法。
如果大家有不明白的我更推荐你从系列的第一篇开始看,内部的实现是一步一步层层递进的。
协程的概念与框架比较大,我本人如有讲解不到位或错漏的地方,希望同学们可以指出交流。
如果感觉本文对你有一点点点的启发,还望你能点赞
支持一下,你的支持是我最大的动力。
Ok,这一期就此完结。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。