Kotlin coroutines - CoroutineScope coroutine scope

Kotlin coroutines - CoroutineScope coroutine scope

Kotlin coroutine series:

In the previous article, we demonstrated the basic use of coroutines and the context of coroutines. In the previous examples, most of us used GlobalScope to start coroutines. Is there no other way to start coroutines? Of course there are, this issue mainly introduces the probability of coroutine scope and some common coroutine scopes.

The scope of the coroutine involves the scope of code running, the life cycle of the coroutine, and the automatic management of the life cycle of the coroutine. Let's first look at the life cycle and scope of the coroutine

First, the life cycle of the coroutine

When we create a coroutine, we will return a Job object, whether it is managed by return value or by the constructor of launch, it is actually the same.

We can get the running status of the current coroutine through Job, and we can also cancel the coroutine at any time.

Coroutine status query

  • isActive
  • isCompleted
  • isCancelled

Commonly used coroutine operations:

  • cancel is used to cancel the Job, cancel the coroutine
  • start is used to start a coroutine and let it reach the Active state
  • invokeOnCompletion adds a listener that will be called when the job is complete or when an exception occurs
  • join blocks and waits for the current coroutine to complete

Isn't the coroutine started by default? How come there is a start method.

In fact, the coroutine is started by default, but we can create a lazy loaded coroutine and start the coroutine manually.

val job = GlobalScope.launch(start = CoroutineStart.LAZY) {

    YYLogUtils.w("执行在协程中...")

    delay(1000L)

    YYLogUtils.w("执行完毕...")
}


job.start()
复制代码

The cancellation of the coroutine, as we have mentioned before, generally we can call calcel manually or call calcel when onDestory:

var job: Job = Job()

...

 GlobalScope.launch(job) {

   YYLogUtils.w("执行在协程中...")

    delay(1000L)

    YYLogUtils.w("执行完毕...")
}


override fun onDestroy() {
    job.cancel()
    super.onDestroy()
}
复制代码

The callback invokeOnCompletion after the execution of the coroutine is also a commonly used listener. This method will be called back after normal execution or abnormal execution.

      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("完成或异常的回调")
        }
复制代码

Callback without exception:

The callback after adding the 9/0 exception code:

Second, the scope of the coroutine

When we create a coroutine, we will need a CoroutineScope, which is the scope of the coroutine. We generally use its launch function and async function to create the coroutine.

2.1 Commonly used coroutine scope

之前我们都是通过 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的作用域范围的使用等等。

其实到这里协程的讲解就差不多了,下一篇我们会讲一下协程的场景使用,网络请求的使用,自定义协程,与协程的并发与锁等相关用法。

如果大家有不明白的我更推荐你从系列的第一篇开始看,内部的实现是一步一步层层递进的。

协程的概念与框架比较大,我本人如有讲解不到位或错漏的地方,希望同学们可以指出交流。

If you feel that this article has inspired you a little, I hope you can 点赞support me. Your support is my biggest motivation.

Ok, this is the end of this issue.

I am participating in the recruitment of the creator signing program of the Nuggets Technology Community, click the link to register and submit .

Guess you like

Origin juejin.im/post/7120023947717902373